浏览代码

Merge branch 'metrics-tab-v3'

Torkel Ödegaard 8 年之前
父节点
当前提交
2fc67da69a
共有 35 个文件被更改,包括 404 次插入383 次删除
  1. 1 1
      pkg/api/api.go
  2. 4 3
      pkg/api/plugins.go
  3. 23 7
      pkg/plugins/datasource_plugin.go
  4. 2 5
      pkg/plugins/models.go
  5. 9 14
      pkg/plugins/plugins.go
  6. 0 1
      public/app/core/components/code_editor/code_editor.ts
  7. 1 0
      public/app/core/core.ts
  8. 6 12
      public/app/core/utils/kbn.js
  9. 32 0
      public/app/core/utils/outline.js
  10. 38 1
      public/app/features/panel/metrics_tab.ts
  11. 0 9
      public/app/features/panel/panel_ctrl.ts
  12. 89 37
      public/app/features/panel/partials/metrics_tab.html
  13. 15 8
      public/app/features/panel/query_troubleshooter.ts
  14. 4 5
      public/app/features/plugins/plugin_edit_ctrl.ts
  15. 1 1
      public/app/features/templating/interval_variable.ts
  16. 9 8
      public/app/plugins/datasource/elasticsearch/partials/config.html
  17. 0 38
      public/app/plugins/datasource/elasticsearch/partials/query.options.html
  18. 5 1
      public/app/plugins/datasource/elasticsearch/plugin.json
  19. 10 0
      public/app/plugins/datasource/elasticsearch/query_help.md
  20. 13 0
      public/app/plugins/datasource/graphite/datasource.ts
  21. 0 5
      public/app/plugins/datasource/graphite/module.ts
  22. 0 123
      public/app/plugins/datasource/graphite/partials/query.options.html
  23. 5 0
      public/app/plugins/datasource/graphite/plugin.json
  24. 30 0
      public/app/plugins/datasource/graphite/query_help.md
  25. 0 5
      public/app/plugins/datasource/influxdb/module.ts
  26. 9 5
      public/app/plugins/datasource/influxdb/partials/config.html
  27. 0 76
      public/app/plugins/datasource/influxdb/partials/query.options.html
  28. 4 0
      public/app/plugins/datasource/influxdb/plugin.json
  29. 28 0
      public/app/plugins/datasource/influxdb/query_help.md
  30. 1 1
      public/sass/_variables.dark.scss
  31. 5 5
      public/sass/components/_gf-form.scss
  32. 20 10
      public/sass/components/_infobox.scss
  33. 27 0
      public/sass/components/_query_editor.scss
  34. 11 0
      public/sass/mixins/_animations.scss
  35. 2 2
      public/test/core/utils/kbn_specs.js

+ 1 - 1
pkg/api/api.go

@@ -209,7 +209,7 @@ func (hs *HttpServer) registerRoutes() {
 
 		r.Get("/plugins", wrap(GetPluginList))
 		r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
-		r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
+		r.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
 
 		r.Group("/plugins", func() {
 			r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))

+ 4 - 3
pkg/api/plugins.go

@@ -147,15 +147,16 @@ func GetPluginDashboards(c *middleware.Context) Response {
 	}
 }
 
-func GetPluginReadme(c *middleware.Context) Response {
+func GetPluginMarkdown(c *middleware.Context) Response {
 	pluginId := c.Params(":pluginId")
+	name := c.Params(":name")
 
-	if content, err := plugins.GetPluginReadme(pluginId); err != nil {
+	if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil {
 		if notfound, ok := err.(plugins.PluginNotFoundError); ok {
 			return ApiError(404, notfound.Error(), nil)
 		}
 
-		return ApiError(500, "Could not get readme", err)
+		return ApiError(500, "Could not get markdown file", err)
 	} else {
 		return Respond(200, content)
 	}

+ 23 - 7
pkg/plugins/datasource_plugin.go

@@ -1,15 +1,22 @@
 package plugins
 
-import "encoding/json"
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+)
 
 type DataSourcePlugin struct {
 	FrontendPluginBase
-	Annotations bool              `json:"annotations"`
-	Metrics     bool              `json:"metrics"`
-	Alerting    bool              `json:"alerting"`
-	BuiltIn     bool              `json:"builtIn"`
-	Mixed       bool              `json:"mixed"`
-	Routes      []*AppPluginRoute `json:"routes"`
+	Annotations  bool            `json:"annotations"`
+	Metrics      bool            `json:"metrics"`
+	Alerting     bool            `json:"alerting"`
+	QueryOptions map[string]bool `json:"queryOptions,omitempty"`
+	BuiltIn      bool            `json:"builtIn,omitempty"`
+	Mixed        bool            `json:"mixed,omitempty"`
+	HasQueryHelp bool            `json:"hasQueryHelp,omitempty"`
+
+	Routes []*AppPluginRoute `json:"-"`
 }
 
 func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
@@ -21,6 +28,15 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
 		return err
 	}
 
+	// look for help markdown
+	helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md")
+	if _, err := os.Stat(helpPath); os.IsNotExist(err) {
+		helpPath = filepath.Join(p.PluginDir, "query_help.md")
+	}
+	if _, err := os.Stat(helpPath); err == nil {
+		p.HasQueryHelp = true
+	}
+
 	DataSources[p.Id] = p
 	return nil
 }

+ 2 - 5
pkg/plugins/models.go

@@ -38,8 +38,8 @@ type PluginBase struct {
 	Includes     []*PluginInclude   `json:"includes"`
 	Module       string             `json:"module"`
 	BaseUrl      string             `json:"baseUrl"`
-	HideFromList bool               `json:"hideFromList"`
-	State        string             `json:"state"`
+	HideFromList bool               `json:"hideFromList,omitempty"`
+	State        string             `json:"state,omitempty"`
 
 	IncludedInAppId string `json:"-"`
 	PluginDir       string `json:"-"`
@@ -48,9 +48,6 @@ type PluginBase struct {
 
 	GrafanaNetVersion   string `json:"-"`
 	GrafanaNetHasUpdate bool   `json:"-"`
-
-	// cache for readme file contents
-	Readme []byte `json:"-"`
 }
 
 func (pb *PluginBase) registerPlugin(pluginDir string) error {

+ 9 - 14
pkg/plugins/plugins.go

@@ -3,6 +3,7 @@ package plugins
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path"
@@ -166,30 +167,24 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
 	return loader.Load(jsonParser, currentDir)
 }
 
-func GetPluginReadme(pluginId string) ([]byte, error) {
+func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
 	plug, exists := Plugins[pluginId]
 	if !exists {
 		return nil, PluginNotFoundError{pluginId}
 	}
 
-	if plug.Readme != nil {
-		return plug.Readme, nil
+	path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
+	if _, err := os.Stat(path); os.IsNotExist(err) {
+		path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
 	}
 
-	readmePath := filepath.Join(plug.PluginDir, "README.md")
-	if _, err := os.Stat(readmePath); os.IsNotExist(err) {
-		readmePath = filepath.Join(plug.PluginDir, "readme.md")
+	if _, err := os.Stat(path); os.IsNotExist(err) {
+		return make([]byte, 0), nil
 	}
 
-	if _, err := os.Stat(readmePath); os.IsNotExist(err) {
-		plug.Readme = make([]byte, 0)
-		return plug.Readme, nil
-	}
-
-	if readmeBytes, err := ioutil.ReadFile(readmePath); err != nil {
+	if data, err := ioutil.ReadFile(path); err != nil {
 		return nil, err
 	} else {
-		plug.Readme = readmeBytes
-		return plug.Readme, nil
+		return data, nil
 	}
 }

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

@@ -159,7 +159,6 @@ function link(scope, elem, attrs) {
         enableSnippets: true
       });
 
-      console.log('getting completer', lang);
       if (scope.getCompleter()) {
         // make copy of array as ace seems to share completers array between instances
         codeEditor.completers = codeEditor.completers.slice();

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

@@ -20,6 +20,7 @@ import './jquery_extended';
 import './partials';
 import './components/jsontree/jsontree';
 import './components/code_editor/code_editor';
+import './utils/outline';
 
 import {grafanaAppDirective} from './components/grafana_app';
 import {sideMenuDirective} from './components/sidemenu/sidemenu';

+ 6 - 12
public/app/core/utils/kbn.js

@@ -163,21 +163,15 @@ function($, _) {
     ms: 0.001
   };
 
-  kbn.calculateInterval = function(range, resolution, userInterval) {
+  kbn.calculateInterval = function(range, resolution, lowLimitInterval) {
     var lowLimitMs = 1; // 1 millisecond default low limit
-    var intervalMs, lowLimitInterval;
+    var intervalMs;
 
-    if (userInterval) {
-      if (userInterval[0] === '>') {
-        lowLimitInterval = userInterval.slice(1);
-        lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
-      }
-      else {
-        return {
-          intervalMs: kbn.interval_to_ms(userInterval),
-          interval: userInterval,
-        };
+    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);

+ 32 - 0
public/app/core/utils/outline.js

@@ -0,0 +1,32 @@
+// outline.js
+// based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/
+(function(d) {
+  "use strict";
+
+  var style_element = d.createElement('STYLE'),
+    dom_events = 'addEventListener' in d,
+    add_event_listener = function(type, callback) {
+      // Basic cross-browser event handling
+      if(dom_events){
+        d.addEventListener(type, callback);
+      } else {
+        d.attachEvent('on' + type, callback);
+      }
+    },
+    set_css = function(css_text) {
+      // Handle setting of <style> element contents in IE8
+      !!style_element.styleSheet ? style_element.styleSheet.cssText = css_text : style_element.innerHTML = css_text;
+    };
+
+  d.getElementsByTagName('HEAD')[0].appendChild(style_element);
+
+  // Using mousedown instead of mouseover, so that previously focused elements don't lose focus ring on mouse move
+  add_event_listener('mousedown', function() {
+    set_css(':focus{outline:0 !important}::-moz-focus-inner{border:0;}');
+  });
+
+  add_event_listener('keydown', function() {
+    set_css('');
+  });
+
+})(document);

+ 38 - 1
public/app/features/panel/metrics_tab.ts

@@ -2,6 +2,7 @@
 
 import _ from 'lodash';
 import {DashboardModel} from '../dashboard/model';
+import Remarkable from 'remarkable';
 
 export class MetricsTabCtrl {
   dsName: string;
@@ -13,9 +14,15 @@ export class MetricsTabCtrl {
   dashboard: DashboardModel;
   panelDsValue: any;
   addQueryDropdown: any;
+  queryTroubleshooterOpen: boolean;
+  helpOpen: boolean;
+  optionsOpen: boolean;
+  hasQueryHelp: boolean;
+  helpHtml: string;
+  queryOptions: any;
 
   /** @ngInject */
-  constructor($scope, private uiSegmentSrv, private datasourceSrv) {
+  constructor($scope, private $sce, private datasourceSrv, private backendSrv, private $timeout) {
     this.panelCtrl = $scope.ctrl;
     $scope.ctrl = this;
 
@@ -33,6 +40,12 @@ export class MetricsTabCtrl {
     this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
     // update next ref id
     this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
+    this.updateDatasourceOptions();
+  }
+
+  updateDatasourceOptions() {
+    this.hasQueryHelp = this.current.meta.hasQueryHelp;
+    this.queryOptions = this.current.meta.queryOptions;
   }
 
   getOptions(includeBuiltin) {
@@ -50,6 +63,7 @@ export class MetricsTabCtrl {
 
     this.current = option.datasource;
     this.panelCtrl.setDatasource(option.datasource);
+    this.updateDatasourceOptions();
   }
 
   addMixedQuery(option) {
@@ -65,6 +79,29 @@ export class MetricsTabCtrl {
   addQuery() {
     this.panelCtrl.addQuery({isNew: true});
   }
+
+  toggleHelp() {
+    this.optionsOpen = false;
+    this.queryTroubleshooterOpen = false;
+    this.helpOpen = !this.helpOpen;
+
+    this.backendSrv.get(`/api/plugins/${this.current.meta.id}/markdown/query_help`).then(res => {
+      var md = new Remarkable();
+      this.helpHtml = this.$sce.trustAsHtml(md.render(res));
+    });
+  }
+
+  toggleOptions() {
+    this.helpOpen = false;
+    this.queryTroubleshooterOpen = false;
+    this.optionsOpen = !this.optionsOpen;
+  }
+
+  toggleQueryTroubleshooter() {
+    this.helpOpen = false;
+    this.optionsOpen = false;
+    this.queryTroubleshooterOpen = !this.queryTroubleshooterOpen;
+  }
 }
 
 /** @ngInject **/

+ 0 - 9
public/app/features/panel/panel_ctrl.ts

@@ -29,7 +29,6 @@ export class PanelCtrl {
   fullscreen: boolean;
   inspector: any;
   editModeInitiated: boolean;
-  editorHelpIndex: number;
   editMode: any;
   height: any;
   containerHeight: any;
@@ -186,14 +185,6 @@ export class PanelCtrl {
     this.events.emit('render', payload);
   }
 
-  toggleEditorHelp(index) {
-    if (this.editorHelpIndex === index) {
-      this.editorHelpIndex = null;
-      return;
-    }
-    this.editorHelpIndex = index;
-  }
-
   duplicate() {
     this.dashboard.duplicatePanel(this.panel, this.row);
     this.$timeout(() => {

+ 89 - 37
public/app/features/panel/partials/metrics_tab.html

@@ -1,9 +1,84 @@
+<div class="gf-form-group">
+  <div class="gf-form-inline">
+    <div class="gf-form">
+			<label class="gf-form-label gf-query-ds-label">
+				<i class="icon-gf icon-gf-datasources"></i>
+			</label>
+      <label class="gf-form-label">Data Source</label>
+
+      <gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
+                        lookup-text="true"
+                        get-options="ctrl.getOptions(true)"
+                        on-change="ctrl.datasourceChanged($option)">
+      </gf-form-dropdown>
+		</div>
+
+		<div class="gf-form gf-form--grow">
+			<label class="gf-form-label gf-form-label--grow"></label>
+		</div>
+		<div class="gf-form" ng-if="ctrl.queryOptions">
+			<a class="gf-form-label" ng-click="ctrl.toggleOptions()">
+				<i class="fa fa-fw fa-caret-right" ng-hide="ctrl.optionsOpen"></i><i class="fa fa-fw fa-caret-down" ng-show="ctrl.optionsOpen"></i>Options
+			</a>
+		</div>
+		<div class="gf-form" ng-if="ctrl.hasQueryHelp">
+			<button class="gf-form-label" ng-click="ctrl.toggleHelp()">
+				<i class="fa fa-fw fa-caret-right" ng-hide="ctrl.helpOpen"></i><i class="fa fa-fw fa-caret-down" ng-show="ctrl.helpOpen"></i>Help
+			</button>
+		</div>
+		<div class="gf-form">
+			<button class="gf-form-label" ng-click="ctrl.toggleQueryTroubleshooter()" bs-tooltip="'Display query request & response'">
+				<i class="fa fa-fw fa-caret-right" ng-hide="ctrl.queryTroubleshooterOpen"></i><i class="fa fa-fw fa-caret-down" ng-show="ctrl.queryTroubleshooterOpen"></i>Query Inspector
+			</button>
+		</div>
+	</div>
+
+	<div>
+		<div ng-if="ctrl.optionsOpen">
+			<div class="gf-form gf-form--flex-end" ng-if="ctrl.queryOptions.minInterval">
+				<label class="gf-form-label">Min time interval</label>
+				<input type="text" class="gf-form-input width-6" placeholder="{{ctrl.panelCtrl.interval}}" ng-model="ctrl.panel.interval" spellcheck="false" ng-model-onblur ng-change="ctrl.panelCtrl.refresh()" />
+				<info-popover mode="right-absolute">
+					A lower limit for the auto group by time interval. Recommended to be set to write frequency,
+					for example <code>1m</code> if your data is written every minute. Access auto interval via variable <code>$__interval</code> for time range
+					string and <code>$__interval_ms</code> for numeric variable that can be used in math expressions.
+				</info-popover>
+			</div>
+			<div class="gf-form gf-form--flex-end" ng-if="ctrl.queryOptions.cacheTimeout">
+				<label class="gf-form-label width-9">Cache timeout</label>
+				<input  type="text" class="gf-form-input width-6" placeholder="60" ng-model="ctrl.panel.cacheTimeout" ng-model-onblur ng-change="ctrl.panelCtrl.refresh()" spellcheck="false" />
+				<info-popover mode="right-absolute">
+					If your time series store has a query cache this option can override the default
+					cache timeout. Specify a numeric value in seconds.
+				</info-popover>
+			</div>
+			<div class="gf-form gf-form--flex-end" ng-if="ctrl.queryOptions.maxDataPoints">
+				<label class="gf-form-label width-9">Max data points</label>
+				<input type="text" class="gf-form-input width-6" placeholder="auto" ng-model-onblur ng-change="ctrl.panelCtrl.refresh()" ng-model="ctrl.panel.maxDataPoints" spellcheck="false"  />
+				<info-popover mode="right-absolute">
+					The maximum data points the query should return. For graphs this
+					is automatically set to one data point per pixel.
+				</info-popover>
+			</div>
+		</div>
+
+		<div class="grafana-info-box" ng-if="ctrl.helpOpen">
+			<div class="markdown-html" ng-bind-html="ctrl.helpHtml"></div>
+			<a class="grafana-info-box__close" ng-click="ctrl.toggleHelp()">
+				<i class="fa fa-chevron-up"></i>
+			</a>
+		</div>
+
+		<query-troubleshooter panel-ctrl="ctrl.panelCtrl" is-open="ctrl.queryTroubleshooterOpen"></query-troubleshooter>
+	</div>
+</div>
+
 <div class="query-editor-rows gf-form-group">
-  <div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
-    <rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
-      <plugin-component type="query-ctrl">
-      </plugin-component>
-    </rebuild-on-change>
+	<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
+		<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
+			<plugin-component type="query-ctrl">
+			</plugin-component>
+		</rebuild-on-change>
 	</div>
 
 	<div class="gf-form-query">
@@ -14,37 +89,14 @@
 				</span>
 				<span class="gf-form-query-letter-cell-letter">{{ctrl.panelCtrl.nextRefId}}</span>
 			</label>
-      <button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed">
-        Add Query
-      </button>
-
-      <div class="dropdown" ng-if="ctrl.current.meta.mixed">
-        <gf-form-dropdown model="ctrl.addQueryDropdown"
-                          get-options="ctrl.getOptions(false)"
-                          on-change="ctrl.addMixedQuery($option)">
-        </gf-form-dropdown>
-      </div>
-    </div>
-  </div>
-</div>
-
-<!-- <query&#45;troubleshooter panel&#45;ctrl="ctrl.panelCtrl"></query&#45;troubleshooter> -->
-
-<div class="gf-form-group">
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <label class="gf-form-label">Panel Data Source</label>
-      <gf-form-dropdown model="ctrl.panelDsValue"
-                        lookup-text="true"
-                        get-options="ctrl.getOptions(true)"
-                        on-change="ctrl.datasourceChanged($option)">
-      </gf-form-dropdown>
-    </div>
-  </div>
-</div>
+			<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed">
+				Add Query
+			</button>
 
-<rebuild-on-change property="ctrl.panel.datasource" show-null="true">
-  <plugin-component type="query-options-ctrl">
-  </plugin-component>
-</rebuild-on-change>
+			<div class="dropdown" ng-if="ctrl.current.meta.mixed">
+				<gf-form-dropdown model="ctrl.addQueryDropdown" get-options="ctrl.getOptions(false)" on-change="ctrl.addMixedQuery($option)">
+				</gf-form-dropdown>
+			</div>
+		</div>
+	</div>
 </div>

+ 15 - 8
public/app/features/panel/query_troubleshooter.ts

@@ -5,9 +5,8 @@ import appEvents  from 'app/core/app_events';
 import {coreModule, JsonExplorer} from 'app/core/core';
 
 const template = `
-<collapse-box title="Query Troubleshooter" is-open="ctrl.isOpen" state-changed="ctrl.stateChanged()"
-              ng-class="{'collapse-box--error': ctrl.hasError}">
-  <collapse-box-actions>
+<div class="query-troubleshooter" ng-if="ctrl.isOpen">
+  <div class="query-troubleshooter__header">
     <a class="pointer" ng-click="ctrl.toggleExpand()" ng-hide="ctrl.allNodesExpanded">
       <i class="fa fa-plus-square-o"></i> Expand All
     </a>
@@ -15,12 +14,12 @@ const template = `
       <i class="fa fa-minus-square-o"></i> Collapse All
     </a>
     <a class="pointer" clipboard-button="ctrl.getClipboardText()"><i class="fa fa-clipboard"></i> Copy to Clipboard</a>
-  </collapse-box-actions>
-  <collapse-box-body>
+  </div>
+  <div class="query-troubleshooter__body">
     <i class="fa fa-spinner fa-spin" ng-show="ctrl.isLoading"></i>
     <div class="query-troubleshooter-json"></div>
-  </collapse-box-body>
-</collapse-box>
+  </div>
+</div>
 `;
 
 export class QueryTroubleshooterCtrl {
@@ -42,7 +41,9 @@ export class QueryTroubleshooterCtrl {
 
     appEvents.on('ds-request-response', this.onRequestResponseEventListener);
     appEvents.on('ds-request-error', this.onRequestErrorEventListener);
+
     $scope.$on('$destroy',  this.removeEventsListeners.bind(this));
+    $scope.$watch('ctrl.isOpen',  this.stateChanged.bind(this));
   }
 
   removeEventsListeners() {
@@ -51,6 +52,11 @@ export class QueryTroubleshooterCtrl {
   }
 
   onRequestError(err) {
+    // ignore if closed
+    if (!this.isOpen) {
+      return;
+    }
+
     this.isOpen = true;
     this.hasError = true;
     this.onRequestResponse(err);
@@ -133,7 +139,8 @@ export function queryTroubleshooter() {
     bindToController: true,
     controllerAs: 'ctrl',
     scope: {
-      panelCtrl: "="
+      panelCtrl: "=",
+      isOpen: "=",
     },
     link: function(scope, elem, attrs, ctrl) {
 

+ 4 - 5
public/app/features/plugins/plugin_edit_ctrl.ts

@@ -3,6 +3,7 @@
 import angular from 'angular';
 import _ from 'lodash';
 import appEvents from 'app/core/app_events';
+import Remarkable from 'remarkable';
 
 export class PluginEditCtrl {
   model: any;
@@ -67,11 +68,9 @@ export class PluginEditCtrl {
   }
 
   initReadme() {
-    return this.backendSrv.get(`/api/plugins/${this.pluginId}/readme`).then(res => {
-      return System.import('remarkable').then(Remarkable => {
-        var md = new Remarkable();
-        this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
-      });
+    return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
+      var md = new Remarkable();
+      this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
     });
   }
 

+ 1 - 1
public/app/features/templating/interval_variable.ts

@@ -54,7 +54,7 @@ export class IntervalVariable implements Variable {
       this.options.unshift({ text: 'auto', value: '$__auto_interval' });
     }
 
-    var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
+    var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, this.auto_min);
     this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval);
   }
 

+ 9 - 8
public/app/plugins/datasource/elasticsearch/partials/config.html

@@ -25,13 +25,14 @@
 		<span class="gf-form-label width-9">Version</span>
 		<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.esVersion" ng-options="f.value as f.name for f in ctrl.esVersions"></select>
 	</div>
-
-</div>
-
-<h3 class="page-heading">Default query settings</h3>
-<div class="gf-form-group">
-	<div class="gf-form">
-		<span class="gf-form-label">Group by time interval</span>
-		<input class="gf-form-input max-width-9" type="text" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="example: >10s">
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<span class="gf-form-label width-9">Min interval</span>
+			<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
+			<info-popover mode="right-absolute">
+				A lower limit for the auto group by time interval. Recommended to be set to write frequency,
+				for example <code>1m</code> if your data is written every minute.
+			</info-popover>
+		</div>
 	</div>
 </div>

+ 0 - 38
public/app/plugins/datasource/elasticsearch/partials/query.options.html

@@ -1,38 +0,0 @@
-<section class="grafana-metric-options">
-	<div class="gf-form-group">
-		<div class="gf-form">
-			<span class="gf-form-label">
-				<i class="fa fa-wrench"></i>
-			</span>
-			<span class="gf-form-label">Group by time interval</span>
-
-			<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
-							 spellcheck='false' placeholder="example: >10s">
-
-			<span class="gf-form-label">
-				<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >60s'" data-placement="right"></i>
-			</span>
-		</div>
-			<div class="gf-form">
-				<span class="gf-form-label">
-					<i class="fa fa-info-circle"></i>
-				</span>
-				<span class="gf-form-label width-23">
-					<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-						alias patterns
-					</a>
-				</span>
-		</div>
-	</div>
-</section>
-
-<div class="pull-left">
-	<div class="grafana-info-box" style="border: 0;"  ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
-		<h5>Alias patterns</h5>
-		<ul ng-non-bindable>
-			<li>{{term fieldname}} = replaced with value of term group by</li>
-			<li>{{metric}} = replaced with metric name (ex. Average, Min, Max)</li>
-			<li>{{field}} = replaced with the metric field name</li>
-		</ul>
-	</div>
-</div>

+ 5 - 1
public/app/plugins/datasource/elasticsearch/plugin.json

@@ -21,5 +21,9 @@
   },
 
   "annotations": true,
-  "metrics": true
+  "metrics": true,
+
+  "queryOptions": {
+    "minInterval": true
+  }
 }

+ 10 - 0
public/app/plugins/datasource/elasticsearch/query_help.md

@@ -0,0 +1,10 @@
+#### Alias patterns
+- {{term fieldname}} = replaced with value of term group by
+- {{metric}} = replaced with metric name (ex. Average, Min, Max)
+- {{field}} = replaced with the metric field name
+
+#### Documentation links
+
+[Grafana's Elasticsearch Documentation](http://docs.grafana.org/features/datasources/elasticsearch)
+
+[Official Elasticsearch Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html)

+ 13 - 0
public/app/plugins/datasource/graphite/datasource.ts

@@ -16,6 +16,19 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
   this.withCredentials = instanceSettings.withCredentials;
   this.render_method = instanceSettings.render_method || 'POST';
 
+  this.getQueryOptionsInfo = function() {
+    return {
+      "maxDataPoints": true,
+      "cacheTimeout": true,
+      "links": [
+        {
+          text: "Help",
+          url: "http://docs.grafana.org/features/datasources/graphite/#using-graphite-in-grafana"
+        }
+      ]
+    };
+  };
+
   this.query = function(options) {
     var graphOptions = {
       from: this.translateTime(options.rangeRaw.from, false),

+ 0 - 5
public/app/plugins/datasource/graphite/module.ts

@@ -2,10 +2,6 @@ import {GraphiteDatasource} from './datasource';
 import {GraphiteQueryCtrl} from './query_ctrl';
 import {GraphiteConfigCtrl} from './config_ctrl';
 
-class GraphiteQueryOptionsCtrl {
-  static templateUrl = 'partials/query.options.html';
-}
-
 class AnnotationsQueryCtrl {
   static templateUrl = 'partials/annotations.editor.html';
 }
@@ -14,7 +10,6 @@ export {
   GraphiteDatasource as Datasource,
   GraphiteQueryCtrl as QueryCtrl,
   GraphiteConfigCtrl as ConfigCtrl,
-  GraphiteQueryOptionsCtrl as QueryOptionsCtrl,
   AnnotationsQueryCtrl as AnnotationsQueryCtrl,
 };
 

+ 0 - 123
public/app/plugins/datasource/graphite/partials/query.options.html

@@ -1,123 +0,0 @@
-<section class="gf-form-group">
-	<div class="gf-form-inline">
-		<div class="gf-form max-width-15">
-			<span class="gf-form-label width-8">
-				Cache timeout
-			</span>
-			<input type="text"
-				class="gf-form-input"
-				ng-model="ctrl.panelCtrl.panel.cacheTimeout"
-				bs-tooltip="'Graphite parameter to override memcache default timeout (unit is seconds)'"
-				data-placement="right"
-				spellcheck='false'
-				placeholder="60">
-			</input>
-		</div>
-		<div class="gf-form max-width-15">
-			<span class="gf-form-label">Max data points</span>
-			<input type="text"
-				class="gf-form-input"
-				ng-model="ctrl.panelCtrl.panel.maxDataPoints"
-				bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
-				data-placement="right"
-				ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
-				spellcheck='false'
-				placeholder="auto">
-			</input>
-		</div>
-	</div>
-	<div class="gf-form-inline">
-		<div class="gf-form">
-			<span class="gf-form-label width-12">
-				<i class="fa fa-info-circle"></i>
-				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-					Shorter legend names
-				</a>
-			</span>
-			<span class="gf-form-label width-12">
-				<i class="fa fa-info-circle"></i>
-				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-					Series as parameters
-				</a>
-			</span>
-			<span class="gf-form-label width-7">
-				<i class="fa fa-info-circle"></i>
-				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-					Stacking
-				</a>
-			</span>
-			<span class="gf-form-label width-8">
-				<i class="fa fa-info-circle"></i>
-				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-					Templating
-				</a>
-			</span>
-			<span class="gf-form-label width-10">
-				<i class="fa fa-info-circle"></i>
-				<a ng-click="ctrl.panelCtrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-					max data points
-				</a>
-			</span>
-		</div>
-	</div>
-</section>
-
-<div class="editor-row">
-	<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
-		<h5>Shorter legend names</h5>
-		<ul>
-			<li>alias() function to specify a custom series name</li>
-			<li>aliasByNode(2) to alias by a specific part of your metric path</li>
-			<li>aliasByNode(2, -1) you can add multiple segment paths, and use negative index</li>
-			<li>groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by</li>
-		</ul>
-	</div>
-
-	<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
-		<h5>Series as parameter</h5>
-		<ul>
-			<li>Some graphite functions allow you to have many series arguments</li>
-			<li>Use #[A-Z] to use a graphite query as parameter to a function</li>
-			<li>
-				Examples:
-				<ul>
-					<li>asPercent(#A, #B)</li>
-					<li>prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query</li>
-					<li>prod.srv-01.counters.count - sumSeries(#A) : sum count and series A </li>
-					<li>divideSeries(#A, #B)</li>
-				</ul>
-			</li>
-			<li>If a query is added only to be used as a parameter, hide it from the graph with the eye icon</li>
-		</ul>
-	</div>
-
-	<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
-		<h5>Stacking</h5>
-		<ul>
-			<li>You find the stacking option under Display Styles tab</li>
-			<li>When stacking is enabled make sure null point mode is set to 'null as zero'</li>
-		</ul>
-	</div>
-
-	<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 4">
-		<h5>Templating</h5>
-		<ul>
-			<li>You can use a template variable in place of metric names</li>
-			<li>You can use a template variable in place of function parameters</li>
-			<li>You enable the templating feature in Dashboard settings / Feature toggles </li>
-		</ul>
-	</div>
-
-	<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 5">
-		<h5>Max data points</h5>
-		<ul>
-			<li>Every graphite request is issued with a maxDataPoints parameter</li>
-			<li>Graphite uses this parameter to consolidate the real number of values down to this number</li>
-			<li>If there are more real values, then by default they will be consolidated using averages</li>
-			<li>This could hide real peaks and max values in your series</li>
-			<li>You can change how point consolidation is made using the consolidateBy graphite function</li>
-			<li>Point consolidation will effect series legend values (min,max,total,current)</li>
-			<li>If you override maxDataPoint and set a high value performance can be severely effected</li>
-		</ul>
-	</div>
-</div>

+ 5 - 0
public/app/plugins/datasource/graphite/plugin.json

@@ -11,6 +11,11 @@
   "alerting": true,
   "annotations": true,
 
+  "queryOptions": {
+    "maxDataPoints": true,
+    "cacheTimeout": true
+  },
+
   "info": {
     "author": {
       "name": "Grafana Project",

+ 30 - 0
public/app/plugins/datasource/graphite/query_help.md

@@ -0,0 +1,30 @@
+#### Get Shorter legend names
+
+- alias() function to specify a custom series name
+- aliasByNode(2) to alias by a specific part of your metric path
+- groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by.
+
+#### Series as parameter
+
+- Some graphite functions allow you to have many series arguments
+- Use #[A-Z] to use a graphite query as parameter to a function
+- Examples:
+  - asPercent(#A, #B)
+  - divideSeries(#A, #B)
+
+If a query is added only to be used as a parameter, hide it from the graph with the eye icon
+
+#### Max data points
+- Every graphite request is issued with a maxDataPoints parameter
+- Graphite uses this parameter to consolidate the real number of values down to this number
+- If there are more real values, then by default they will be consolidated using averages
+- This could hide real peaks and max values in your series
+- You can change how point consolidation is made using the consolidateBy graphite function
+- Point consolidation will effect series legend values (min,max,total,current)
+- if you override maxDataPoint and set a high value performance can be severely effected
+
+#### Documentation links:
+
+[Grafana's Graphite Documentation](http://docs.grafana.org/features/datasources/graphite)
+
+[Official Graphite Documentation](https://graphite.readthedocs.io)

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

@@ -5,10 +5,6 @@ class InfluxConfigCtrl {
   static templateUrl = 'partials/config.html';
 }
 
-class InfluxQueryOptionsCtrl {
-  static templateUrl = 'partials/query.options.html';
-}
-
 class InfluxAnnotationsQueryCtrl {
   static templateUrl = 'partials/annotations.editor.html';
 }
@@ -17,7 +13,6 @@ export {
   InfluxDatasource as Datasource,
   InfluxQueryCtrl as QueryCtrl,
   InfluxConfigCtrl as ConfigCtrl,
-  InfluxQueryOptionsCtrl as QueryOptionsCtrl,
   InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl,
 };
 

+ 9 - 5
public/app/plugins/datasource/influxdb/partials/config.html

@@ -24,10 +24,14 @@
 </div>
 
 <div class="gf-form-group">
-	<div class="gf-form max-width-21">
-		<span class="gf-form-label">Default group by time</span>
-		<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval"
-		spellcheck='false' placeholder="example: >10s"></input>
-		<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >10s'" data-placement="right"></i>
+	<div class="gf-form-inline">
+		<div class="gf-form">
+			<span class="gf-form-label">Min time interval</span>
+			<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
+			<info-popover mode="right-absolute">
+				A lower limit for the auto group by time interval. Recommended to be set to write frequency,
+				for example <code>1m</code> if your data is written every minute.
+			</info-popover>
+		</div>
 	</div>
 </div>

+ 0 - 76
public/app/plugins/datasource/influxdb/partials/query.options.html

@@ -1,76 +0,0 @@
-<section class="grafana-metric-options">
-	<div class="gf-form-group">
-		<div class="gf-form-inline">
-			<div class="gf-form">
-				<span class="gf-form-label"><i class="fa fa-wrench"></i></span>
-				<span class="gf-form-label width-11">Group by time interval</span>
-				<input type="text" class="gf-form-input width-16" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
-				spellcheck='false' placeholder="example: >10s">
-				<info-popover mode="right-absolute">
-          Set a low limit by having a greater sign: example: >60s
-        </info-popover>
-			</div>
-		</div>
-		<div class="gf-form-inline">
-			<div class="gf-form">
-				<span class="gf-form-label width-10">
-					<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-						<i class="fa fa-info-circle"></i>
-						&nbsp;alias patterns
-					</a>
-				</span>
-				<span class="gf-form-label width-10">
-					<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-					<i class="fa fa-info-circle"></i>
-						&nbsp;stacking &amp; fill
-					</a>
-				</span>
-				<span class="gf-form-label width-10">
-					<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
-					<i class="fa fa-info-circle"></i>
-						&nbsp;group by time
-					</a>
-				</span>
-			</div>
-		</div>
-	</div>
-</section>
-
-<div class="editor-row">
-	<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
-		<h5>Alias patterns</h5>
-		<ul>
-			<li>$m = replaced with measurement name</li>
-			<li>$measurement = replaced with measurement name</li>
-			<li>$1 - $9 = replaced with part of measurement name (if you separate your measurement name with dots)</li>
-			<li>$col = replaced with column name</li>
-			<li>$tag_exampletag = replaced with the value of the <i>exampletag</i> tag</li>
-			<li>You can also use [[tag_exampletag]] pattern replacement syntax</li>
-		</ul>
-	</div>
-
-	<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
-		<h5>Stacking and fill</h5>
-		<ul>
-			<li>When stacking is enabled it is important that points align</li>
-			<li>If there are missing points for one series it can cause gaps or missing bars</li>
-			<li>You must use fill(0), and select a group by time low limit</li>
-			<li>Use the group by time option below your queries and specify for example &gt;10s if your metrics are written every 10 seconds</li>
-			<li>This will insert zeros for series that are missing measurements and will make stacking work properly</li>
-		</ul>
-	</div>
-
-	<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
-		<h5>Group by time</h5>
-		<ul>
-			<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
-			<li>Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph</li>
-			<li>If you use fill(0) or fill(null) set a low limit for the auto group by time interval</li>
-			<li>The low limit can only be set in the group by time option below your queries</li>
-			<li>You set a low limit by adding a greater sign before the interval</li>
-			<li>Example: &gt;60s if you write metrics to InfluxDB every 60 seconds</li>
-		</ul>
-	</div>
-</div>
-
-

+ 4 - 0
public/app/plugins/datasource/influxdb/plugin.json

@@ -8,6 +8,10 @@
   "annotations": true,
   "alerting": true,
 
+  "queryOptions": {
+    "minInterval": true
+  },
+
   "info": {
     "author": {
       "name": "Grafana Project",

+ 28 - 0
public/app/plugins/datasource/influxdb/query_help.md

@@ -0,0 +1,28 @@
+#### Alias patterns
+- replaced with measurement name
+- $measurement = replaced with measurement name
+- $1 - $9 = replaced with part of measurement name (if you separate your measurement name with dots)
+- $col = replaced with column name
+- $tag_exampletag = replaced with the value of the <i>exampletag</i> tag
+- You can also use [[tag_exampletag]] pattern replacement syntax
+
+#### Stacking and fill
+- When stacking is enabled it is important that points align
+- If there are missing points for one series it can cause gaps or missing bars
+- You must use fill(0), and select a group by time low limit
+- Use the group by time option below your queries and specify for example &gt;10s if your metrics are written every 10 seconds
+- This will insert zeros for series that are missing measurements and will make stacking work properly
+
+#### Group by time
+- Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
+- Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
+- If you use fill(0) or fill(null) set a low limit for the auto group by time interval
+- The low limit can only be set in the group by time option below your queries
+- You set a low limit by adding a greater sign before the interval
+- Example: &gt;60s if you write metrics to InfluxDB every 60 seconds
+
+#### Documentation links:
+
+[Grafana's InfluxDB Documentation](http://docs.grafana.org/features/datasources/influxdb)
+
+[Official InfluxDB Documentation](https://docs.influxdata.com/influxdb)

+ 1 - 1
public/sass/_variables.dark.scss

@@ -276,7 +276,7 @@ $card-background-hover: linear-gradient(135deg, #343434, #262626);
 $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
 
 // info box
-$info-box-background: linear-gradient(120deg, #142749, #0e203e);
+$info-box-background: linear-gradient(100deg, #1a4552, #0b2127);
 
 // footer
 $footer-link-color:   $gray-1;

+ 5 - 5
public/sass/components/_gf-form.scss

@@ -16,6 +16,10 @@ $gf-form-margin: 0.25rem;
   &--grow {
     flex-grow: 1;
   }
+
+  &--flex-end {
+    justify-content: flex-end;
+  }
 }
 
 .gf-form-disabled {
@@ -54,7 +58,6 @@ $gf-form-margin: 0.25rem;
   background-color: $input-label-bg;
   display: block;
   font-size: $font-size-sm;
-  margin-right: $gf-form-margin;
 
   border: $input-btn-border-width solid transparent;
   @include border-radius($label-border-radius-sm);
@@ -103,7 +106,6 @@ $gf-form-margin: 0.25rem;
   padding: $input-padding-y $input-padding-x;
   margin-right: $gf-form-margin;
   font-size: $font-size-base;
-  margin-right: $gf-form-margin;
   line-height: $input-line-height;
   color: $input-color;
   background-color: $input-bg;
@@ -112,7 +114,6 @@ $gf-form-margin: 0.25rem;
   border: $input-btn-border-width solid $input-border-color;
   @include border-radius($input-border-radius-sm);
   @include box-shadow($input-box-shadow);
-  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -235,7 +236,6 @@ $gf-form-margin: 0.25rem;
   font-size: $font-size-sm;
   box-shadow: none;
   border: $input-btn-border-width solid transparent;
-  @include border-radius($label-border-radius-sm);
 
   flex-shrink: 0;
   flex-grow: 0;
@@ -281,7 +281,7 @@ $gf-form-margin: 0.25rem;
   &--right-absolute {
     position: absolute;
     right: $spacer;
-    top: 8px;
+    top: 10px;
   }
 
   &--right-normal {

+ 20 - 10
public/sass/components/_infobox.scss

@@ -1,12 +1,12 @@
-.grafana-info-box::before {
-  content: "\f05a";
-  font-family:'FontAwesome';
-  position: absolute;
-  top: -13px;
-  left: -8px;
-  font-size: 20px;
-  color: $text-color;
-}
+// .grafana-info-box::before {
+//   content: "\f05a";
+//   font-family:'FontAwesome';
+//   position: absolute;
+//   top: -13px;
+//   left: -8px;
+//   font-size: 20px;
+//   color: $text-color;
+// }
 
 .grafana-info-box {
   position: relative;
@@ -15,12 +15,14 @@
   padding: 1rem;
   border-radius: 4px;
   margin-bottom: $spacer;
+  margin-right: $gf-form-margin;
+  flex-grow: 1;
 
   h5 {
     margin-bottom: $spacer;
   }
   ul {
-    padding-left: $spacer;
+    padding-left: $spacer * 1.5;
   }
 
   a {
@@ -28,3 +30,11 @@
   }
 }
 
+.grafana-info-box__close {
+  text-align: center;
+  display: block;
+  color: $link-color !important;
+  height: 0;
+  position: relative;
+  top: -9px;
+}

+ 27 - 0
public/sass/components/_query_editor.scss

@@ -67,6 +67,11 @@
   }
 }
 
+.gf-query-ds-label {
+  text-align: center;
+  width: 44px;
+}
+
 .grafana-metric-options {
   margin-top: 25px;
 }
@@ -146,3 +151,25 @@ input[type="text"].tight-form-func-param {
     margin-left: 10px;
   }
 }
+
+.query-troubleshooter {
+  font-size: $font-size-sm;
+  margin: $gf-form-margin;
+  border: 1px solid $btn-secondary-bg;
+  min-height: 100px;
+  border-radius: 3px;
+}
+
+.query-troubleshooter__header {
+  float: right;
+  font-size: $font-size-sm;
+  text-align: right;
+  padding: $input-padding-y $input-padding-x;
+  a {
+    margin-left: $spacer;
+  }
+}
+
+.query-troubleshooter__body {
+  padding: $spacer 0;
+}

+ 11 - 0
public/sass/mixins/_animations.scss

@@ -23,3 +23,14 @@
   -o-animation: #{$str};
   animation: #{$str};
 }
+
+.animate-height {
+  max-height: 0;
+  overflow: hidden;
+
+  &--open {
+    max-height: 1000px;
+    overflow: auto;
+    transition: max-height 250ms ease-in-out;
+  }
+}

+ 2 - 2
public/test/core/utils/kbn_specs.js

@@ -143,8 +143,8 @@ define([
       expect(res.intervalMs).to.be(500);
     });
 
-    it('fixed user interval', function() {
-      var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
+    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);