Pārlūkot izejas kodu

Merge branch 'master' into docs-5.1

Marcus Efraimsson 7 gadi atpakaļ
vecāks
revīzija
979f2d79fb

+ 5 - 0
CHANGELOG.md

@@ -16,8 +16,10 @@
 * **Table**: Table plugin value mappings [#7119](https://github.com/grafana/grafana/issues/7119), thx [infernix](https://github.com/infernix)
 * **IE11**: IE 11 compatibility [#11165](https://github.com/grafana/grafana/issues/11165)
 * **Scrolling**: Better scrolling experience [#11053](https://github.com/grafana/grafana/issues/11053), [#11252](https://github.com/grafana/grafana/issues/11252), [#10836](https://github.com/grafana/grafana/issues/10836), [#11185](https://github.com/grafana/grafana/issues/11185), [#11168](https://github.com/grafana/grafana/issues/11168)
+* **Docker**: Improved docker image (breaking changes regarding file ownership) [grafana-docker #141](https://github.com/grafana/grafana-docker/issues/141), thx [@Spindel](https://github.com/Spindel), [@ChristianKniep](https://github.com/ChristianKniep), [@brancz](https://github.com/brancz) and [@jangaraj](https://github.com/jangaraj)
 
 ### Minor
+
 * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
 * **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw)
@@ -45,6 +47,9 @@
 * **Heatmap**: Disable log scale when using time time series buckets [#10792](https://github.com/grafana/grafana/issues/10792)
 * **Provisioning**: Remove `id` from json when provisioning dashboards, [#11138](https://github.com/grafana/grafana/issues/11138)
 * **Prometheus**: tooltip for legend format not showing properly [#11516](https://github.com/grafana/grafana/issues/11516), thx [@svenklemm](https://github.com/svenklemm) 
+* **Playlist**: Empty playlists cannot be deleted [#11133](https://github.com/grafana/grafana/issues/11133), thx [@kichristensen](https://github.com/kichristensen) 
+* **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556)
+* **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm)
 
 ### Tech
 * Migrated JavaScript files to TypeScript

+ 16 - 0
docs/sources/features/datasources/elasticsearch.md

@@ -61,6 +61,22 @@ a time pattern for the index name or a wildcard.
 Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. Currently only 2.x and 5.x
 are supported.
 
+### Min time interval
+A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
+number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+
+Identifier | Description
+------------ | -------------
+`y`   | year
+`M`   | month
+`w`   | week
+`d`   | day
+`h`   | hour
+`m`   | minute
+`s`   | second
+`ms`  | millisecond
+
 ## Metric Query editor
 
 ![Elasticsearch Query Editor](/img/docs/elasticsearch/query_editor.png)

+ 16 - 0
docs/sources/features/datasources/influxdb.md

@@ -43,6 +43,22 @@ All requests will be made from the browser to Grafana backend/server which in tu
 
 All requests will be made from the browser directly to the data source and may be subject to Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access mode.
 
+### Min time interval
+A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
+number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+
+Identifier | Description
+------------ | -------------
+`y`   | year
+`M`   | month
+`w`   | week
+`d`   | day
+`h`   | hour
+`m`   | minute
+`s`   | second
+`ms`  | millisecond
+
 ## Query Editor
 
 {{< docs-imagebox img="/img/docs/v45/influxdb_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v45/influxdb_query.gif" >}}

+ 7 - 1
pkg/api/index.go

@@ -118,9 +118,14 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 	})
 
 	if c.IsSignedIn {
+		// Only set login if it's different from the name
+		var login string
+		if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
+			login = c.SignedInUser.Login
+		}
 		profileNode := &dtos.NavLink{
 			Text:         c.SignedInUser.NameOrFallback(),
-			SubTitle:     c.SignedInUser.Login,
+			SubTitle:     login,
 			Id:           "profile",
 			Img:          data.User.GravatarUrl,
 			Url:          setting.AppSubUrl + "/profile",
@@ -284,6 +289,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 
 	data.NavTree = append(data.NavTree, &dtos.NavLink{
 		Text:         "Help",
+		SubTitle:     fmt.Sprintf(`Grafana v%s (%s)`, setting.BuildVersion, setting.BuildCommit),
 		Id:           "help",
 		Url:          "#",
 		Icon:         "gicon gicon-question",

+ 1 - 1
pkg/api/playlist.go

@@ -33,7 +33,7 @@ func ValidateOrgPlaylist(c *m.ReqContext) {
 		return
 	}
 
-	if len(items) == 0 {
+	if len(items) == 0 && c.Context.Req.Method != "DELETE" {
 		c.JsonApiErr(404, "Playlist is empty", itemsErr)
 		return
 	}

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

@@ -333,6 +333,7 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
 	sess.Join("INNER", "org", "org_user.org_id=org.id")
 	sess.Where("org_user.user_id=?", query.UserId)
 	sess.Cols("org.name", "org_user.role", "org_user.org_id")
+	sess.OrderBy("org.name")
 	err := sess.Find(&query.Result)
 	return err
 }

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

@@ -79,15 +79,15 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		}
 		return fmt.Sprintf("extract(epoch from %s) as \"time\"", args[0]), nil
 	case "__timeFilter":
-		// don't use to_timestamp in this macro for redshift compatibility #9566
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
-		return fmt.Sprintf("extract(epoch from %s) BETWEEN %d AND %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), m.TimeRange.GetToAsSecondsEpoch()), nil
+
+		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeFrom":
-		return fmt.Sprintf("to_timestamp(%d)", m.TimeRange.GetFromAsSecondsEpoch()), nil
+		return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeTo":
-		return fmt.Sprintf("to_timestamp(%d)", m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeGroup":
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)

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

@@ -12,7 +12,7 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := &PostgresMacroEngine{}
+		engine := NewPostgresMacroEngine()
 		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
@@ -38,14 +38,14 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeFrom function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeGroup function", func() {
@@ -68,7 +68,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -102,21 +102,21 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeFrom function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeTo function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -150,21 +150,21 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeFrom function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __timeTo function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
+				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
 			})
 
 			Convey("interpolate __unixEpochFilter function", func() {

+ 8 - 0
pkg/tsdb/time_range.go

@@ -37,6 +37,10 @@ func (tr *TimeRange) GetFromAsSecondsEpoch() int64 {
 	return tr.GetFromAsMsEpoch() / 1000
 }
 
+func (tr *TimeRange) GetFromAsTimeUTC() time.Time {
+	return tr.MustGetFrom().UTC()
+}
+
 func (tr *TimeRange) GetToAsMsEpoch() int64 {
 	return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
 }
@@ -45,6 +49,10 @@ func (tr *TimeRange) GetToAsSecondsEpoch() int64 {
 	return tr.GetToAsMsEpoch() / 1000
 }
 
+func (tr *TimeRange) GetToAsTimeUTC() time.Time {
+	return tr.MustGetTo().UTC()
+}
+
 func (tr *TimeRange) MustGetFrom() time.Time {
 	if res, err := tr.ParseFrom(); err != nil {
 		return time.Unix(0, 0)

+ 4 - 1
public/app/core/components/sidemenu/sidemenu.html

@@ -54,6 +54,9 @@
       </span>
     </a>
     <ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
+      <li ng-if="item.subTitle" class="sidemenu-subtitle">
+        <span class="sidemenu-item-text">{{::item.subTitle}}</span>
+      </li>
       <li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
         <a ng-click="ctrl.switchOrg()">
           <div>
@@ -75,4 +78,4 @@
       </li>
     </ul>
   </div>
-</div>
+</div>

+ 0 - 236
public/app/core/directives/dropdown_typeahead.js

@@ -1,236 +0,0 @@
-define([
-  'lodash',
-  'jquery',
-  '../core_module',
-],
-function (_, $, coreModule) {
-  'use strict';
-
-  coreModule.default.directive('dropdownTypeahead', function($compile) {
-
-    var inputTemplate = '<input type="text"'+
-      ' class="gf-form-input input-medium tight-form-input"' +
-      ' spellcheck="false" style="display:none"></input>';
-
-    var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' +
-      ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
-      ' data-placement="top"><i class="fa fa-plus"></i></a>';
-
-    return {
-      scope: {
-        menuItems: "=dropdownTypeahead",
-        dropdownTypeaheadOnSelect: "&dropdownTypeaheadOnSelect",
-        model: '=ngModel'
-      },
-      link: function($scope, elem, attrs) {
-        var $input = $(inputTemplate);
-        var $button = $(buttonTemplate);
-        $input.appendTo(elem);
-        $button.appendTo(elem);
-
-        if (attrs.linkText) {
-          $button.html(attrs.linkText);
-        }
-
-        if (attrs.ngModel) {
-          $scope.$watch('model', function(newValue) {
-            _.each($scope.menuItems, function(item) {
-              _.each(item.submenu, function(subItem) {
-                if (subItem.value === newValue) {
-                  $button.html(subItem.text);
-                }
-              });
-            });
-          });
-        }
-
-        var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) {
-          if (!value.submenu) {
-            value.click = 'menuItemSelected(' + index + ')';
-            memo.push(value.text);
-          } else {
-            _.each(value.submenu, function(item, subIndex) {
-              item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
-              memo.push(value.text + ' ' + item.text);
-            });
-          }
-          return memo;
-        }, []);
-
-        $scope.menuItemSelected = function(index, subIndex) {
-          var menuItem = $scope.menuItems[index];
-          var payload = {$item: menuItem};
-          if (menuItem.submenu && subIndex !== void 0) {
-            payload.$subItem = menuItem.submenu[subIndex];
-          }
-          $scope.dropdownTypeaheadOnSelect(payload);
-        };
-
-        $input.attr('data-provide', 'typeahead');
-        $input.typeahead({
-          source: typeaheadValues,
-          minLength: 1,
-          items: 10,
-          updater: function (value) {
-            var result = {};
-            _.each($scope.menuItems, function(menuItem) {
-              _.each(menuItem.submenu, function(submenuItem) {
-                if (value === (menuItem.text + ' ' + submenuItem.text)) {
-                  result.$subItem = submenuItem;
-                  result.$item = menuItem;
-                }
-              });
-            });
-
-            if (result.$item) {
-              $scope.$apply(function() {
-                $scope.dropdownTypeaheadOnSelect(result);
-              });
-            }
-
-            $input.trigger('blur');
-            return '';
-          }
-        });
-
-        $button.click(function() {
-          $button.hide();
-          $input.show();
-          $input.focus();
-        });
-
-        $input.keyup(function() {
-          elem.toggleClass('open', $input.val() === '');
-        });
-
-        $input.blur(function() {
-          $input.hide();
-          $input.val('');
-          $button.show();
-          $button.focus();
-          // clicking the function dropdown menu won't
-          // work if you remove class at once
-          setTimeout(function() {
-            elem.removeClass('open');
-          }, 200);
-        });
-
-        $compile(elem.contents())($scope);
-      }
-    };
-  });
-
-  coreModule.default.directive('dropdownTypeahead2', function($compile) {
-
-    var inputTemplate = '<input type="text"'+
-      ' class="gf-form-input"' +
-      ' spellcheck="false" style="display:none"></input>';
-
-    var buttonTemplate = '<a class="gf-form-input dropdown-toggle"' +
-      ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
-      ' data-placement="top"><i class="fa fa-plus"></i></a>';
-
-    return {
-      scope: {
-        menuItems: "=dropdownTypeahead2",
-        dropdownTypeaheadOnSelect: "&dropdownTypeaheadOnSelect",
-        model: '=ngModel'
-      },
-      link: function($scope, elem, attrs) {
-        var $input = $(inputTemplate);
-        var $button = $(buttonTemplate);
-        $input.appendTo(elem);
-        $button.appendTo(elem);
-
-        if (attrs.linkText) {
-          $button.html(attrs.linkText);
-        }
-
-        if (attrs.ngModel) {
-          $scope.$watch('model', function(newValue) {
-            _.each($scope.menuItems, function(item) {
-              _.each(item.submenu, function(subItem) {
-                if (subItem.value === newValue) {
-                  $button.html(subItem.text);
-                }
-              });
-            });
-          });
-        }
-
-        var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) {
-          if (!value.submenu) {
-            value.click = 'menuItemSelected(' + index + ')';
-            memo.push(value.text);
-          } else {
-            _.each(value.submenu, function(item, subIndex) {
-              item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
-              memo.push(value.text + ' ' + item.text);
-            });
-          }
-          return memo;
-        }, []);
-
-        $scope.menuItemSelected = function(index, subIndex) {
-          var menuItem = $scope.menuItems[index];
-          var payload = {$item: menuItem};
-          if (menuItem.submenu && subIndex !== void 0) {
-            payload.$subItem = menuItem.submenu[subIndex];
-          }
-          $scope.dropdownTypeaheadOnSelect(payload);
-        };
-
-        $input.attr('data-provide', 'typeahead');
-        $input.typeahead({
-          source: typeaheadValues,
-          minLength: 1,
-          items: 10,
-          updater: function (value) {
-            var result = {};
-            _.each($scope.menuItems, function(menuItem) {
-              _.each(menuItem.submenu, function(submenuItem) {
-                if (value === (menuItem.text + ' ' + submenuItem.text)) {
-                  result.$subItem = submenuItem;
-                  result.$item = menuItem;
-                }
-              });
-            });
-
-            if (result.$item) {
-              $scope.$apply(function() {
-                $scope.dropdownTypeaheadOnSelect(result);
-              });
-            }
-
-            $input.trigger('blur');
-            return '';
-          }
-        });
-
-        $button.click(function() {
-          $button.hide();
-          $input.show();
-          $input.focus();
-        });
-
-        $input.keyup(function() {
-          elem.toggleClass('open', $input.val() === '');
-        });
-
-        $input.blur(function() {
-          $input.hide();
-          $input.val('');
-          $button.show();
-          $button.focus();
-          // clicking the function dropdown menu won't
-          // work if you remove class at once
-          setTimeout(function() {
-            elem.removeClass('open');
-          }, 200);
-        });
-
-        $compile(elem.contents())($scope);
-      }
-    };
-  });
-});

+ 244 - 0
public/app/core/directives/dropdown_typeahead.ts

@@ -0,0 +1,244 @@
+import _ from 'lodash';
+import $ from 'jquery';
+import coreModule from '../core_module';
+
+/** @ngInject */
+export function dropdownTypeahead($compile) {
+  let inputTemplate =
+    '<input type="text"' +
+    ' class="gf-form-input input-medium tight-form-input"' +
+    ' spellcheck="false" style="display:none"></input>';
+
+  let buttonTemplate =
+    '<a class="gf-form-label tight-form-func dropdown-toggle"' +
+    ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
+    ' data-placement="top"><i class="fa fa-plus"></i></a>';
+
+  return {
+    scope: {
+      menuItems: '=dropdownTypeahead',
+      dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
+      model: '=ngModel',
+    },
+    link: function($scope, elem, attrs) {
+      let $input = $(inputTemplate);
+      let $button = $(buttonTemplate);
+      $input.appendTo(elem);
+      $button.appendTo(elem);
+
+      if (attrs.linkText) {
+        $button.html(attrs.linkText);
+      }
+
+      if (attrs.ngModel) {
+        $scope.$watch('model', function(newValue) {
+          _.each($scope.menuItems, function(item) {
+            _.each(item.submenu, function(subItem) {
+              if (subItem.value === newValue) {
+                $button.html(subItem.text);
+              }
+            });
+          });
+        });
+      }
+
+      let typeaheadValues = _.reduce(
+        $scope.menuItems,
+        function(memo, value, index) {
+          if (!value.submenu) {
+            value.click = 'menuItemSelected(' + index + ')';
+            memo.push(value.text);
+          } else {
+            _.each(value.submenu, function(item, subIndex) {
+              item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
+              memo.push(value.text + ' ' + item.text);
+            });
+          }
+          return memo;
+        },
+        []
+      );
+
+      $scope.menuItemSelected = function(index, subIndex) {
+        let menuItem = $scope.menuItems[index];
+        let payload: any = { $item: menuItem };
+        if (menuItem.submenu && subIndex !== void 0) {
+          payload.$subItem = menuItem.submenu[subIndex];
+        }
+        $scope.dropdownTypeaheadOnSelect(payload);
+      };
+
+      $input.attr('data-provide', 'typeahead');
+      $input.typeahead({
+        source: typeaheadValues,
+        minLength: 1,
+        items: 10,
+        updater: function(value) {
+          let result: any = {};
+          _.each($scope.menuItems, function(menuItem) {
+            _.each(menuItem.submenu, function(submenuItem) {
+              if (value === menuItem.text + ' ' + submenuItem.text) {
+                result.$subItem = submenuItem;
+                result.$item = menuItem;
+              }
+            });
+          });
+
+          if (result.$item) {
+            $scope.$apply(function() {
+              $scope.dropdownTypeaheadOnSelect(result);
+            });
+          }
+
+          $input.trigger('blur');
+          return '';
+        },
+      });
+
+      $button.click(function() {
+        $button.hide();
+        $input.show();
+        $input.focus();
+      });
+
+      $input.keyup(function() {
+        elem.toggleClass('open', $input.val() === '');
+      });
+
+      $input.blur(function() {
+        $input.hide();
+        $input.val('');
+        $button.show();
+        $button.focus();
+        // clicking the function dropdown menu won't
+        // work if you remove class at once
+        setTimeout(function() {
+          elem.removeClass('open');
+        }, 200);
+      });
+
+      $compile(elem.contents())($scope);
+    },
+  };
+}
+
+/** @ngInject */
+export function dropdownTypeahead2($compile) {
+  let inputTemplate =
+    '<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
+
+  let buttonTemplate =
+    '<a class="gf-form-input dropdown-toggle"' +
+    ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
+    ' data-placement="top"><i class="fa fa-plus"></i></a>';
+
+  return {
+    scope: {
+      menuItems: '=dropdownTypeahead2',
+      dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
+      model: '=ngModel',
+    },
+    link: function($scope, elem, attrs) {
+      let $input = $(inputTemplate);
+      let $button = $(buttonTemplate);
+      $input.appendTo(elem);
+      $button.appendTo(elem);
+
+      if (attrs.linkText) {
+        $button.html(attrs.linkText);
+      }
+
+      if (attrs.ngModel) {
+        $scope.$watch('model', function(newValue) {
+          _.each($scope.menuItems, function(item) {
+            _.each(item.submenu, function(subItem) {
+              if (subItem.value === newValue) {
+                $button.html(subItem.text);
+              }
+            });
+          });
+        });
+      }
+
+      let typeaheadValues = _.reduce(
+        $scope.menuItems,
+        function(memo, value, index) {
+          if (!value.submenu) {
+            value.click = 'menuItemSelected(' + index + ')';
+            memo.push(value.text);
+          } else {
+            _.each(value.submenu, function(item, subIndex) {
+              item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
+              memo.push(value.text + ' ' + item.text);
+            });
+          }
+          return memo;
+        },
+        []
+      );
+
+      $scope.menuItemSelected = function(index, subIndex) {
+        let menuItem = $scope.menuItems[index];
+        let payload: any = { $item: menuItem };
+        if (menuItem.submenu && subIndex !== void 0) {
+          payload.$subItem = menuItem.submenu[subIndex];
+        }
+        $scope.dropdownTypeaheadOnSelect(payload);
+      };
+
+      $input.attr('data-provide', 'typeahead');
+      $input.typeahead({
+        source: typeaheadValues,
+        minLength: 1,
+        items: 10,
+        updater: function(value) {
+          let result: any = {};
+          _.each($scope.menuItems, function(menuItem) {
+            _.each(menuItem.submenu, function(submenuItem) {
+              if (value === menuItem.text + ' ' + submenuItem.text) {
+                result.$subItem = submenuItem;
+                result.$item = menuItem;
+              }
+            });
+          });
+
+          if (result.$item) {
+            $scope.$apply(function() {
+              $scope.dropdownTypeaheadOnSelect(result);
+            });
+          }
+
+          $input.trigger('blur');
+          return '';
+        },
+      });
+
+      $button.click(function() {
+        $button.hide();
+        $input.show();
+        $input.focus();
+      });
+
+      $input.keyup(function() {
+        elem.toggleClass('open', $input.val() === '');
+      });
+
+      $input.blur(function() {
+        $input.hide();
+        $input.val('');
+        $button.show();
+        $button.focus();
+        // clicking the function dropdown menu won't
+        // work if you remove class at once
+        setTimeout(function() {
+          elem.removeClass('open');
+        }, 200);
+      });
+
+      $compile(elem.contents())($scope);
+    },
+  };
+}
+
+coreModule.directive('dropdownTypeahead', dropdownTypeahead);
+coreModule.directive('dropdownTypeahead2', dropdownTypeahead2);

+ 1 - 1
public/app/core/directives/value_select_dropdown.ts

@@ -142,7 +142,7 @@ export class ValueSelectDropdownCtrl {
     commitChange = commitChange || false;
     excludeOthers = excludeOthers || false;
 
-    let setAllExceptCurrentTo = function(newValue) {
+    let setAllExceptCurrentTo = newValue => {
       _.each(this.options, other => {
         if (option !== other) {
           other.selected = newValue;

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

@@ -35,7 +35,7 @@
     </div>
 	<div class="gf-form-inline">
 		<div class="gf-form">
-			<span class="gf-form-label width-9">Min interval</span>
+			<span class="gf-form-label width-9">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,

+ 2 - 3
public/app/plugins/datasource/influxdb/query_help.md

@@ -10,7 +10,7 @@
 - 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
+- Use the group by time option below your queries and specify for example 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
@@ -18,8 +18,7 @@
 - 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
+- Example: 60s if you write metrics to InfluxDB every 60 seconds
 
 #### Documentation links:
 

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

@@ -8,7 +8,7 @@ class PostgresConfigCtrl {
 
   /** @ngInject **/
   constructor($scope) {
-    this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'require';
+    this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
   }
 }
 

+ 4 - 4
public/app/plugins/datasource/postgres/partials/annotations.editor.html

@@ -28,12 +28,12 @@ An annotation is an event that is overlaid on top of graphs. The query can have
 Macros:
 - $__time(column) -&gt; column as "time"
 - $__timeEpoch -&gt; extract(epoch from column) as "time"
-- $__timeFilter(column) -&gt;  column &ge; to_timestamp(1492750877) AND column &le; to_timestamp(1492750877)
-- $__unixEpochFilter(column) -&gt;  column &gt; 1492750877 AND column &lt; 1492750877
+- $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
+- $__unixEpochFilter(column) -&gt;  column &gt;= 1492750877 AND column &lt;= 1492750877
 
 Or build your own conditionals using these macros which just return the values:
-- $__timeFrom() -&gt;  to_timestamp(1492750877)
-- $__timeTo() -&gt;  to_timestamp(1492750877)
+- $__timeFrom() -&gt;  '2017-04-21T05:01:17Z'
+- $__timeTo() -&gt;  '2017-04-21T05:01:17Z'
 - $__unixEpochFrom() -&gt;  1492750877
 - $__unixEpochTo() -&gt;  1492750877
 		</pre>

+ 4 - 4
public/app/plugins/datasource/postgres/partials/query.editor.html

@@ -48,8 +48,8 @@ Table:
 Macros:
 - $__time(column) -&gt; column as "time"
 - $__timeEpoch -&gt; extract(epoch from column) as "time"
-- $__timeFilter(column) -&gt;  extract(epoch from column) BETWEEN 1492750877 AND 1492750877
-- $__unixEpochFilter(column) -&gt;  column &gt; 1492750877 AND column &lt; 1492750877
+- $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
+- $__unixEpochFilter(column) -&gt;  column &gt;= 1492750877 AND column &lt;= 1492750877
 - $__timeGroup(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300 AS time
 
 Example of group by and order by with $__timeGroup:
@@ -61,8 +61,8 @@ GROUP BY time
 ORDER BY time
 
 Or build your own conditionals using these macros which just return the values:
-- $__timeFrom() -&gt;  to_timestamp(1492750877)
-- $__timeTo() -&gt;  to_timestamp(1492750877)
+- $__timeFrom() -&gt;  '2017-04-21T05:01:17Z'
+- $__timeTo() -&gt;  '2017-04-21T05:01:17Z'
 - $__unixEpochFrom() -&gt;  1492750877
 - $__unixEpochTo() -&gt;  1492750877
 		</pre>

+ 9 - 0
public/sass/components/_sidemenu.scss

@@ -149,6 +149,15 @@
   color: #ebedf2;
 }
 
+.sidemenu-subtitle {
+  padding: 0.5rem 1rem 0.5rem;
+  font-size: $font-size-sm;
+  color: $text-color-weak;
+  border-bottom: 1px solid $dropdownDividerBottom;
+  margin-bottom: 0.25rem;
+  white-space: nowrap;
+}
+
 li.sidemenu-org-switcher {
   border-bottom: 1px solid $dropdownDividerBottom;
 }