Ver Fonte

Merge branch '14293-metric-display-names' into reactify-stackdriver

# Conflicts:
#	public/app/plugins/datasource/stackdriver/partials/query.aggregation.html
#	public/app/plugins/datasource/stackdriver/partials/query.editor.html
#	public/app/plugins/datasource/stackdriver/partials/query.filter.html
#	public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts
#	public/app/plugins/datasource/stackdriver/query_ctrl.ts
#	public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
#	public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts
Erik Sundell há 7 anos atrás
pai
commit
49144f07e4
44 ficheiros alterados com 561 adições e 367 exclusões
  1. 3 3
      docs/sources/http_api/admin.md
  2. 6 15
      docs/sources/installation/debian.md
  3. 5 11
      docs/sources/installation/rpm.md
  4. 20 29
      pkg/components/dashdiffs/formatter_json.go
  5. 1 1
      pkg/services/dashboards/dashboard_service.go
  6. 4 4
      pkg/services/sqlstore/login_attempt.go
  7. 3 3
      pkg/tsdb/postgres/macros.go
  8. 20 3
      pkg/tsdb/postgres/macros_test.go
  9. 4 2
      public/app/core/components/CustomScrollbar/CustomScrollbar.tsx
  10. 4 4
      public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  11. 8 6
      public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
  12. 4 5
      public/app/core/utils/explore.test.ts
  13. 1 1
      public/app/core/utils/explore.ts
  14. 5 3
      public/app/features/alerting/AlertTabCtrl.ts
  15. 159 182
      public/app/features/alerting/partials/alert_tab.html
  16. 80 11
      public/app/features/dashboard/dashgrid/AlertTab.tsx
  17. 13 9
      public/app/features/dashboard/dashgrid/EditorTabBody.tsx
  18. 1 1
      public/app/features/dashboard/dashgrid/PanelEditor.tsx
  19. 8 8
      public/app/features/dashboard/dashgrid/QueriesTab.tsx
  20. 110 0
      public/app/features/dashboard/dashgrid/StateHistory.tsx
  21. 3 3
      public/app/features/dashboard/dashgrid/VisualizationTab.tsx
  22. 1 1
      public/app/features/datasources/settings/BasicSettings.tsx
  23. 1 1
      public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap
  24. 1 1
      public/app/features/datasources/state/reducers.ts
  25. 15 6
      public/app/features/explore/Explore.tsx
  26. 0 1
      public/app/features/explore/LogLabels.tsx
  27. 1 1
      public/app/features/explore/Typeahead.tsx
  28. 0 3
      public/app/features/panel/metrics_tab.ts
  29. 1 1
      public/app/plugins/datasource/influxdb/datasource.ts
  30. 0 1
      public/app/plugins/datasource/loki/components/LokiQueryField.tsx
  31. 0 1
      public/app/plugins/datasource/stackdriver/datasource.ts
  32. 2 2
      public/app/plugins/panel/gauge/module.tsx
  33. 2 5
      public/app/plugins/panel/heatmap/rendering.ts
  34. 1 1
      public/app/types/explore.ts
  35. 55 0
      public/app/viz/Gauge.test.tsx
  36. 8 6
      public/app/viz/Gauge.tsx
  37. 1 1
      public/emails/invited_to_org.html
  38. 1 1
      public/emails/new_user_invite.html
  39. 1 0
      public/sass/components/_form_select_box.scss
  40. 2 0
      public/sass/components/_timepicker.scss
  41. 1 0
      public/sass/pages/_alerting.scss
  42. 1 3
      scripts/webpack/webpack.common.js
  43. 0 1
      scripts/webpack/webpack.prod.js
  44. 4 26
      yarn.lock

+ 3 - 3
docs/sources/http_api/admin.md

@@ -285,7 +285,7 @@ Content-Type: application/json
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{message: "User permissions updated"}
+{"message": "User permissions updated"}
 ```
 ```
 
 
 ## Delete global User
 ## Delete global User
@@ -308,7 +308,7 @@ Content-Type: application/json
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{message: "User deleted"}
+{"message": "User deleted"}
 ```
 ```
 
 
 ## Pause all alerts
 ## Pause all alerts
@@ -339,5 +339,5 @@ JSON Body schema:
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
+{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
 ```
 ```

+ 6 - 15
docs/sources/installation/debian.md

@@ -34,32 +34,23 @@ sudo dpkg -i grafana_<version>_amd64.deb
 Example:
 Example:
 
 
 ```bash
 ```bash
-wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb
+wget https://dl.grafana.com/oss/release/grafana_5.4.2_amd64.deb
 sudo apt-get install -y adduser libfontconfig
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.1.4_amd64.deb
+sudo dpkg -i grafana_5.4.2_amd64.deb
 ```
 ```
 
 
 ## APT Repository
 ## APT Repository
 
 
-Add the following line to your `/etc/apt/sources.list` file.
+Create a file `/etc/apt/sources.list.d/grafana.list` and add the following to it.
 
 
 ```bash
 ```bash
-deb https://packagecloud.io/grafana/stable/debian/ stretch main
+deb https://packages.grafana.com/oss/deb stable main
 ```
 ```
 
 
-Use the above line even if you are on Ubuntu or another Debian version.
-There is also a testing repository if you want beta or release
-candidates.
+Use the above line even if you are on Ubuntu or another Debian version. Then add our gpg key. This allows you to install signed packages.
 
 
 ```bash
 ```bash
-deb https://packagecloud.io/grafana/testing/debian/ stretch main
-```
-
-Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
-allows you to install signed packages.
-
-```bash
-curl https://packagecloud.io/gpg.key | sudo apt-key add -
+curl https://packages.grafana.com/gpg.key | sudo apt-key add -
 ```
 ```
 
 
 Update your Apt repositories and install Grafana
 Update your Apt repositories and install Grafana

+ 5 - 11
docs/sources/installation/rpm.md

@@ -32,7 +32,7 @@ $ sudo yum install <rpm package url>
 Example:
 Example:
 
 
 ```bash
 ```bash
-$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
+$ sudo yum install https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm
 ```
 ```
 
 
 Or install manually using `rpm`. First execute
 Or install manually using `rpm`. First execute
@@ -44,7 +44,7 @@ $ wget <rpm package url>
 Example:
 Example:
 
 
 ```bash
 ```bash
-$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
+$ wget https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm
 ```
 ```
 
 
 ### On CentOS / Fedora / Redhat:
 ### On CentOS / Fedora / Redhat:
@@ -67,21 +67,15 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
 ```bash
 ```bash
 [grafana]
 [grafana]
 name=grafana
 name=grafana
-baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch
+baseurl=https://packages.grafana.com/oss/rpm
 repo_gpgcheck=1
 repo_gpgcheck=1
 enabled=1
 enabled=1
 gpgcheck=1
 gpgcheck=1
-gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana
+gpgkey=https://packages.grafana.com/gpg.key
 sslverify=1
 sslverify=1
 sslcacert=/etc/pki/tls/certs/ca-bundle.crt
 sslcacert=/etc/pki/tls/certs/ca-bundle.crt
 ```
 ```
 
 
-There is also a testing repository if you want beta or release candidates.
-
-```bash
-baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch
-```
-
 Then install Grafana via the `yum` command.
 Then install Grafana via the `yum` command.
 
 
 ```bash
 ```bash
@@ -91,7 +85,7 @@ $ sudo yum install grafana
 ### RPM GPG Key
 ### RPM GPG Key
 
 
 The RPMs are signed, you can verify the signature with this [public GPG
 The RPMs are signed, you can verify the signature with this [public GPG
-key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana).
+key](https://packages.grafana.com/gpg.key).
 
 
 ## Package details
 ## Package details
 
 

+ 20 - 29
pkg/components/dashdiffs/formatter_json.go

@@ -206,10 +206,9 @@ func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []di
 
 
 	// Added
 	// Added
 	for _, delta := range deltas {
 	for _, delta := range deltas {
-		switch delta.(type) {
+		switch delta := delta.(type) {
 		case *diff.Added:
 		case *diff.Added:
-			d := delta.(*diff.Added)
-			f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
+			f.printRecursive(delta.Position.String(), delta.Value, ChangeAdded)
 		}
 		}
 	}
 	}
 
 
@@ -222,9 +221,8 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 	if len(matchedDeltas) > 0 {
 	if len(matchedDeltas) > 0 {
 		for _, matchedDelta := range matchedDeltas {
 		for _, matchedDelta := range matchedDeltas {
 
 
-			switch matchedDelta.(type) {
+			switch matchedDelta := matchedDelta.(type) {
 			case *diff.Object:
 			case *diff.Object:
-				d := matchedDelta.(*diff.Object)
 				switch value.(type) {
 				switch value.(type) {
 				case map[string]interface{}:
 				case map[string]interface{}:
 					//ok
 					//ok
@@ -238,7 +236,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.print("{")
 				f.print("{")
 				f.closeLine()
 				f.closeLine()
 				f.push(positionStr, len(o), false)
 				f.push(positionStr, len(o), false)
-				f.processObject(o, d.Deltas)
+				f.processObject(o, matchedDelta.Deltas)
 				f.pop()
 				f.pop()
 				f.newLine(ChangeNil)
 				f.newLine(ChangeNil)
 				f.print("}")
 				f.print("}")
@@ -246,7 +244,6 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.closeLine()
 				f.closeLine()
 
 
 			case *diff.Array:
 			case *diff.Array:
-				d := matchedDelta.(*diff.Array)
 				switch value.(type) {
 				switch value.(type) {
 				case []interface{}:
 				case []interface{}:
 					//ok
 					//ok
@@ -260,7 +257,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.print("[")
 				f.print("[")
 				f.closeLine()
 				f.closeLine()
 				f.push(positionStr, len(a), true)
 				f.push(positionStr, len(a), true)
-				f.processArray(a, d.Deltas)
+				f.processArray(a, matchedDelta.Deltas)
 				f.pop()
 				f.pop()
 				f.newLine(ChangeNil)
 				f.newLine(ChangeNil)
 				f.print("]")
 				f.print("]")
@@ -268,27 +265,23 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.closeLine()
 				f.closeLine()
 
 
 			case *diff.Added:
 			case *diff.Added:
-				d := matchedDelta.(*diff.Added)
-				f.printRecursive(positionStr, d.Value, ChangeAdded)
+				f.printRecursive(positionStr, matchedDelta.Value, ChangeAdded)
 				f.size[len(f.size)-1]++
 				f.size[len(f.size)-1]++
 
 
 			case *diff.Modified:
 			case *diff.Modified:
-				d := matchedDelta.(*diff.Modified)
 				savedSize := f.size[len(f.size)-1]
 				savedSize := f.size[len(f.size)-1]
-				f.printRecursive(positionStr, d.OldValue, ChangeOld)
+				f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld)
 				f.size[len(f.size)-1] = savedSize
 				f.size[len(f.size)-1] = savedSize
-				f.printRecursive(positionStr, d.NewValue, ChangeNew)
+				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)
 
 
 			case *diff.TextDiff:
 			case *diff.TextDiff:
 				savedSize := f.size[len(f.size)-1]
 				savedSize := f.size[len(f.size)-1]
-				d := matchedDelta.(*diff.TextDiff)
-				f.printRecursive(positionStr, d.OldValue, ChangeOld)
+				f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld)
 				f.size[len(f.size)-1] = savedSize
 				f.size[len(f.size)-1] = savedSize
-				f.printRecursive(positionStr, d.NewValue, ChangeNew)
+				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)
 
 
 			case *diff.Deleted:
 			case *diff.Deleted:
-				d := matchedDelta.(*diff.Deleted)
-				f.printRecursive(positionStr, d.Value, ChangeDeleted)
+				f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted)
 
 
 			default:
 			default:
 				return errors.New("Unknown Delta type detected")
 				return errors.New("Unknown Delta type detected")
@@ -305,13 +298,13 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
 func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
 	results = make([]diff.Delta, 0)
 	results = make([]diff.Delta, 0)
 	for _, delta := range deltas {
 	for _, delta := range deltas {
-		switch delta.(type) {
+		switch typedDelta := delta.(type) {
 		case diff.PostDelta:
 		case diff.PostDelta:
-			if delta.(diff.PostDelta).PostPosition() == position {
+			if typedDelta.PostPosition() == position {
 				results = append(results, delta)
 				results = append(results, delta)
 			}
 			}
 		case diff.PreDelta:
 		case diff.PreDelta:
-			if delta.(diff.PreDelta).PrePosition() == position {
+			if typedDelta.PrePosition() == position {
 				results = append(results, delta)
 				results = append(results, delta)
 			}
 			}
 		default:
 		default:
@@ -417,20 +410,19 @@ func (f *JSONFormatter) print(a string) {
 }
 }
 
 
 func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
 func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
-	switch value.(type) {
+	switch value := value.(type) {
 	case map[string]interface{}:
 	case map[string]interface{}:
 		f.newLine(change)
 		f.newLine(change)
 		f.printKey(name)
 		f.printKey(name)
 		f.print("{")
 		f.print("{")
 		f.closeLine()
 		f.closeLine()
 
 
-		m := value.(map[string]interface{})
-		size := len(m)
+		size := len(value)
 		f.push(name, size, false)
 		f.push(name, size, false)
 
 
-		keys := sortKeys(m)
+		keys := sortKeys(value)
 		for _, key := range keys {
 		for _, key := range keys {
-			f.printRecursive(key, m[key], change)
+			f.printRecursive(key, value[key], change)
 		}
 		}
 		f.pop()
 		f.pop()
 
 
@@ -445,10 +437,9 @@ func (f *JSONFormatter) printRecursive(name string, value interface{}, change Ch
 		f.print("[")
 		f.print("[")
 		f.closeLine()
 		f.closeLine()
 
 
-		s := value.([]interface{})
-		size := len(s)
+		size := len(value)
 		f.push("", size, true)
 		f.push("", size, true)
-		for _, item := range s {
+		for _, item := range value {
 			f.printRecursive("", item, change)
 			f.printRecursive("", item, change)
 		}
 		}
 		f.pop()
 		f.pop()

+ 1 - 1
pkg/services/dashboards/dashboard_service.go

@@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
 		return nil, models.ErrDashboardFolderCannotHaveParent
 		return nil, models.ErrDashboardFolderCannotHaveParent
 	}
 	}
 
 
-	if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) {
+	if dash.IsFolder && strings.EqualFold(dash.Title, models.RootFolderName) {
 		return nil, models.ErrDashboardFolderNameExists
 		return nil, models.ErrDashboardFolderNameExists
 	}
 	}
 
 

+ 4 - 4
pkg/services/sqlstore/login_attempt.go

@@ -78,14 +78,14 @@ func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error {
 }
 }
 
 
 func toInt64(i interface{}) int64 {
 func toInt64(i interface{}) int64 {
-	switch i.(type) {
+	switch i := i.(type) {
 	case []byte:
 	case []byte:
-		n, _ := strconv.ParseInt(string(i.([]byte)), 10, 64)
+		n, _ := strconv.ParseInt(string(i), 10, 64)
 		return n
 		return n
 	case int:
 	case int:
-		return int64(i.(int))
+		return int64(i)
 	case int64:
 	case int64:
-		return i.(int64)
+		return i
 	}
 	}
 	return 0
 	return 0
 }
 }

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

@@ -86,11 +86,11 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 		}
 
 
-		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
 	case "__timeFrom":
 	case "__timeFrom":
-		return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano)), nil
 	case "__timeTo":
 	case "__timeTo":
-		return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
 	case "__timeGroup":
 	case "__timeGroup":
 		if len(args) < 2 {
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
 			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)

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

@@ -41,7 +41,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
 			})
 			})
 
 
 			Convey("interpolate __timeFrom function", func() {
 			Convey("interpolate __timeFrom function", func() {
@@ -138,7 +138,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
 			})
 			})
 
 
 			Convey("interpolate __unixEpochFilter function", func() {
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -158,7 +158,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
-				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
 			})
 			})
 
 
 			Convey("interpolate __unixEpochFilter function", func() {
 			Convey("interpolate __unixEpochFilter function", func() {
@@ -168,5 +168,22 @@ func TestMacroEngine(t *testing.T) {
 				So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
 				So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
 			})
 			})
 		})
 		})
+
+		Convey("Given a time range between 1960-02-01 07:00:00.5 and 1980-02-03 08:00:00.5", func() {
+			from := time.Date(1960, 2, 1, 7, 0, 0, 500e6, time.UTC)
+			to := time.Date(1980, 2, 3, 8, 0, 0, 500e6, time.UTC)
+			timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
+
+			So(from.Format(time.RFC3339Nano), ShouldEqual, "1960-02-01T07:00:00.5Z")
+			So(to.Format(time.RFC3339Nano), ShouldEqual, "1980-02-03T08:00:00.5Z")
+			Convey("interpolate __timeFilter function", func() {
+				sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
+			})
+
+		})
+
 	})
 	})
 }
 }

+ 4 - 2
public/app/core/components/CustomScrollbar/CustomScrollbar.tsx

@@ -28,8 +28,10 @@ class CustomScrollbar extends PureComponent<Props> {
       <Scrollbars
       <Scrollbars
         className={customClassName}
         className={customClassName}
         autoHeight={true}
         autoHeight={true}
-        autoHeightMin={'inherit'}
-        autoHeightMax={'inherit'}
+        // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
+        // Before these where set to inhert but that caused problems with cut of legends in firefox
+        autoHeightMin={'0'}
+        autoHeightMax={'100%'}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}

+ 4 - 4
public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap

@@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
   style={
   style={
     Object {
     Object {
       "height": "auto",
       "height": "auto",
-      "maxHeight": "inherit",
-      "minHeight": "inherit",
+      "maxHeight": "100%",
+      "minHeight": "0",
       "overflow": "hidden",
       "overflow": "hidden",
       "position": "relative",
       "position": "relative",
       "width": "100%",
       "width": "100%",
@@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
         "left": undefined,
         "left": undefined,
         "marginBottom": 0,
         "marginBottom": 0,
         "marginRight": 0,
         "marginRight": 0,
-        "maxHeight": "calc(inherit + 0px)",
-        "minHeight": "calc(inherit + 0px)",
+        "maxHeight": "calc(100% + 0px)",
+        "minHeight": "calc(0 + 0px)",
         "overflow": "scroll",
         "overflow": "scroll",
         "position": "relative",
         "position": "relative",
         "right": undefined,
         "right": undefined,

+ 8 - 6
public/app/core/components/EmptyListCTA/EmptyListCTA.tsx

@@ -24,12 +24,14 @@ class EmptyListCTA extends Component<Props, any> {
           <i className={buttonIcon} />
           <i className={buttonIcon} />
           {buttonTitle}
           {buttonTitle}
         </a>
         </a>
-        <div className="empty-list-cta__pro-tip">
-          <i className="fa fa-rocket" /> ProTip: {proTip}
-          <a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
-            {proTipLinkTitle}
-          </a>
-        </div>
+        {proTip && (
+          <div className="empty-list-cta__pro-tip">
+            <i className="fa fa-rocket" /> ProTip: {proTip}
+            <a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
+              {proTipLinkTitle}
+            </a>
+          </div>
+        )}
       </div>
       </div>
     );
     );
   }
   }

+ 4 - 5
public/app/core/utils/explore.test.ts

@@ -14,7 +14,6 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
   datasourceError: null,
   datasourceError: null,
   datasourceLoading: null,
   datasourceLoading: null,
   datasourceMissing: false,
   datasourceMissing: false,
-  datasourceName: '',
   exploreDatasources: [],
   exploreDatasources: [],
   graphInterval: 1000,
   graphInterval: 1000,
   history: [],
   history: [],
@@ -69,7 +68,7 @@ describe('state functions', () => {
     it('returns url parameter value for a state object', () => {
     it('returns url parameter value for a state object', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        datasourceName: 'foo',
+        initialDatasource: 'foo',
         range: {
         range: {
           from: 'now-5h',
           from: 'now-5h',
           to: 'now',
           to: 'now',
@@ -94,7 +93,7 @@ describe('state functions', () => {
     it('returns url parameter value for a state object', () => {
     it('returns url parameter value for a state object', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        datasourceName: 'foo',
+        initialDatasource: 'foo',
         range: {
         range: {
           from: 'now-5h',
           from: 'now-5h',
           to: 'now',
           to: 'now',
@@ -120,7 +119,7 @@ describe('state functions', () => {
     it('can parse the serialized state into the original state', () => {
     it('can parse the serialized state into the original state', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        datasourceName: 'foo',
+        initialDatasource: 'foo',
         range: {
         range: {
           from: 'now - 5h',
           from: 'now - 5h',
           to: 'now',
           to: 'now',
@@ -144,7 +143,7 @@ describe('state functions', () => {
       const resultState = {
       const resultState = {
         ...rest,
         ...rest,
         datasource: DEFAULT_EXPLORE_STATE.datasource,
         datasource: DEFAULT_EXPLORE_STATE.datasource,
-        datasourceName: datasource,
+        initialDatasource: datasource,
         initialQueries: queries,
         initialQueries: queries,
       };
       };
 
 

+ 1 - 1
public/app/core/utils/explore.ts

@@ -105,7 +105,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
 
 
 export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
 export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
   const urlState: ExploreUrlState = {
   const urlState: ExploreUrlState = {
-    datasource: state.datasourceName,
+    datasource: state.initialDatasource,
     queries: state.initialQueries.map(clearQueryKeys),
     queries: state.initialQueries.map(clearQueryKeys),
     range: state.range,
     range: state.range,
   };
   };

+ 5 - 3
public/app/features/alerting/AlertTabCtrl.ts

@@ -45,6 +45,7 @@ export class AlertTabCtrl {
     this.noDataModes = alertDef.noDataModes;
     this.noDataModes = alertDef.noDataModes;
     this.executionErrorModes = alertDef.executionErrorModes;
     this.executionErrorModes = alertDef.executionErrorModes;
     this.appSubUrl = config.appSubUrl;
     this.appSubUrl = config.appSubUrl;
+    this.panelCtrl._enableAlert = this.enable;
   }
   }
 
 
   $onInit() {
   $onInit() {
@@ -114,7 +115,7 @@ export class AlertTabCtrl {
   }
   }
 
 
   getNotifications() {
   getNotifications() {
-    return Promise.resolve(
+    return this.$q.when(
       this.notifications.map(item => {
       this.notifications.map(item => {
         return this.uiSegmentSrv.newSegment(item.name);
         return this.uiSegmentSrv.newSegment(item.name);
       })
       })
@@ -147,6 +148,7 @@ export class AlertTabCtrl {
     // reset plus button
     // reset plus button
     this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
     this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
     this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html;
     this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html;
+    this.addNotificationSegment.fake = true;
   }
   }
 
 
   removeNotification(index) {
   removeNotification(index) {
@@ -353,11 +355,11 @@ export class AlertTabCtrl {
     });
     });
   }
   }
 
 
-  enable() {
+  enable = () => {
     this.panel.alert = {};
     this.panel.alert = {};
     this.initModel();
     this.initModel();
     this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
     this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
-  }
+  };
 
 
   evaluatorParamsChanged() {
   evaluatorParamsChanged() {
     ThresholdMapper.alertToGraphThresholds(this.panel);
     ThresholdMapper.alertToGraphThresholds(this.panel);

+ 159 - 182
public/app/features/alerting/partials/alert_tab.html

@@ -1,191 +1,168 @@
-<div class="panel-option-section__body" ng-if="ctrl.alert">
-	<div class="edit-tab-with-sidemenu">
-		<aside class="edit-sidemenu-aside">
-			<ul class="edit-sidemenu">
-				<li ng-class="{active: ctrl.subTabIndex === 0}">
-					<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
-				</li>
-				<li ng-class="{active: ctrl.subTabIndex === 1}">
-					<a ng-click="ctrl.changeTabIndex(1)">
-						Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
-					</a>
-				</li>
-				<li ng-class="{active: ctrl.subTabIndex === 2}">
-					<a ng-click="ctrl.changeTabIndex(2)">State history</a>
-				</li>
-				<li>
-					<a ng-click="ctrl.delete()">Delete</a>
-				</li>
-			</ul>
-		</aside>
+<div ng-if="ctrl.panel.alert">
+  <div class="alert alert-error m-b-2" ng-show="ctrl.error">
+    <i class="fa fa-warning"></i> {{ctrl.error}}
+  </div>
+  <div class="panel-option-section">
+    <div class="panel-option-section__body">
+      <div class="gf-form-group">
+        <h4 class="section-heading">Rule</h4>
+        <div class="gf-form-inline">
+          <div class="gf-form">
+            <span class="gf-form-label width-6">Name</span>
+            <input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
+          </div>
+          <div class="gf-form">
+            <span class="gf-form-label width-9">Evaluate every</span>
+            <input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
+          </div>
+          <div class="gf-form max-width-11">
+            <label class="gf-form-label width-5">For</label>
+            <input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for"
+                  spellcheck='false' placeholder="5m">
+            <info-popover mode="right-absolute">
+              If an alert rule has a configured For and the query violates the configured
+              threshold it
+              will first go from OK to Pending.
+              Going from OK to Pending Grafana will not send any notifications. Once the alert
+              rule
+              has
+              been firing for more than For duration, it will change to Alerting and send alert
+              notifications.
+            </info-popover>
+          </div>
+        </div>
+      </div>
 
 
-		<div class="edit-tab-content">
-			<div ng-if="ctrl.subTabIndex === 0">
-				<div class="alert alert-error m-b-2" ng-show="ctrl.error">
-					<i class="fa fa-warning"></i> {{ctrl.error}}
-				</div>
+      <div class="gf-form-group">
+        <h4 class="section-heading">Conditions</h4>
+        <div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
+          <div class="gf-form">
+            <metric-segment-model css-class="query-keyword width-5" ng-if="$index"
+                                  property="conditionModel.operator.type" options="ctrl.evalOperators"
+                                  custom="false"></metric-segment-model>
+            <span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
+          </div>
+          <div class="gf-form">
+            <query-part-editor class="gf-form-label query-part width-9"
+                                part="conditionModel.reducerPart"
+                                handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
+            </query-part-editor>
+            <span class="gf-form-label query-keyword">OF</span>
+          </div>
+          <div class="gf-form">
+            <query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart"
+                                handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
+            </query-part-editor>
+          </div>
+          <div class="gf-form">
+            <metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions"
+                                  custom="false" css-class="query-keyword"
+                                  on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
+            <input class="gf-form-input max-width-9" type="number" step="any"
+                    ng-hide="conditionModel.evaluator.params.length === 0"
+                    ng-model="conditionModel.evaluator.params[0]"
+                    ng-change="ctrl.evaluatorParamsChanged()" />
+            <label class="gf-form-label query-keyword"
+                    ng-show="conditionModel.evaluator.params.length === 2">TO</label>
+            <input class="gf-form-input max-width-9" type="number" step="any"
+                    ng-if="conditionModel.evaluator.params.length === 2"
+                    ng-model="conditionModel.evaluator.params[1]"
+                    ng-change="ctrl.evaluatorParamsChanged()" />
+          </div>
+          <div class="gf-form">
+            <label class="gf-form-label">
+              <a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
+                <i class="fa fa-trash"></i>
+              </a>
+            </label>
+          </div>
+        </div>
 
 
-				<div class="gf-form-group">
-					<h5 class="section-heading">Alert Config</h5>
-					<div class="gf-form">
-						<span class="gf-form-label width-6">Name</span>
-						<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
-					</div>
-					<div class="gf-form-inline">
-						<div class="gf-form">
-							<span class="gf-form-label width-9">Evaluate every</span>
-							<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
-						</div>
-						<div class="gf-form max-width-11">
-							<label class="gf-form-label width-5">For</label>
-							<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for" spellcheck='false' placeholder="5m">
-							<info-popover mode="right-absolute">
-									If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending.
-									Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
-							</info-popover>
-						</div>
-					</div>
-				</div>
+        <div class="gf-form">
+          <label class="gf-form-label dropdown">
+            <a class="pointer dropdown-toggle" data-toggle="dropdown">
+              <i class="fa fa-plus"></i>
+            </a>
+            <ul class="dropdown-menu" role="menu">
+              <li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
+                <a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
+              </li>
+            </ul>
+          </label>
+        </div>
+      </div>
 
 
-				<div class="gf-form-group">
-					<h5 class="section-heading">Conditions</h5>
-					<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
-						<div class="gf-form">
-							<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
-							<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
-						</div>
-									<div class="gf-form">
-							<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
-							</query-part-editor>
-										<span class="gf-form-label query-keyword">OF</span>
-						</div>
-						<div class="gf-form">
-							<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
-							</query-part-editor>
-						</div>
-						<div class="gf-form">
-							<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
-							<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()">
-							<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
-							<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()">
-						</div>
-						<div class="gf-form">
-							<label class="gf-form-label">
-								<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
-									<i class="fa fa-trash"></i>
-								</a>
-							</label>
-						</div>
-					</div>
+      <div class="gf-form-group">
+        <h4 class="section-heading">No Data & Error Handling</h4>
+        <div class="gf-form-inline">
+          <div class="gf-form">
+            <span class="gf-form-label width-15">If no data or all values are null</span>
+          </div>
+          <div class="gf-form">
+            <span class="gf-form-label query-keyword">SET STATE TO</span>
+            <div class="gf-form-select-wrapper">
+              <select class="gf-form-input" ng-model="ctrl.alert.noDataState"
+                                            ng-options="f.value as f.text for f in ctrl.noDataModes">
+              </select>
+            </div>
+          </div>
+        </div>
 
 
-					<div class="gf-form">
-						<label class="gf-form-label dropdown">
-							<a class="pointer dropdown-toggle" data-toggle="dropdown">
-								<i class="fa fa-plus"></i>
-							</a>
-							<ul class="dropdown-menu" role="menu">
-								<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
-									<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
-								</li>
-							</ul>
-						</label>
-					</div>
-				</div>
+        <div class="gf-form-inline">
+          <div class="gf-form">
+            <span class="gf-form-label width-15">If execution error or timeout</span>
+          </div>
+          <div class="gf-form">
+            <span class="gf-form-label query-keyword">SET STATE TO</span>
+            <div class="gf-form-select-wrapper">
+              <select class="gf-form-input" ng-model="ctrl.alert.executionErrorState"
+                                            ng-options="f.value as f.text for f in ctrl.executionErrorModes">
+              </select>
+            </div>
+          </div>
+        </div>
 
 
-				<div class="gf-form-group">
-					<div class="gf-form">
-									<span class="gf-form-label width-18">If no data or all values are null</span>
-									<span class="gf-form-label query-keyword">SET STATE TO</span>
-						<div class="gf-form-select-wrapper">
-							<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
-							</select>
-						</div>
-					</div>
+        <div class="gf-form-button-row">
+          <button class="btn btn-inverse" ng-click="ctrl.test()">
+            Test Rule
+          </button>
+        </div>
+      </div>
 
 
-					<div class="gf-form">
-									<span class="gf-form-label width-18">If execution error or timeout</span>
-									<span class="gf-form-label query-keyword">SET STATE TO</span>
-						<div class="gf-form-select-wrapper">
-							<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
-							</select>
-						</div>
-					</div>
+      <div class="gf-form-group" ng-if="ctrl.testing">
+        Evaluating rule <i class="fa fa-spinner fa-spin"></i>
+      </div>
 
 
-					<div class="gf-form-button-row">
-						<button class="btn btn-inverse" ng-click="ctrl.test()">
-							Test Rule
-						</button>
-					</div>
-				</div>
+      <div class="gf-form-group" ng-if="ctrl.testResult">
+        <json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
+      </div>
+    </div>
+  </div>
 
 
-				<div class="gf-form-group" ng-if="ctrl.testing">
-					Evaluating rule <i class="fa fa-spinner fa-spin"></i>
-				</div>
-
-				<div class="gf-form-group" ng-if="ctrl.testResult">
-					<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
-				</div>
-			</div>
-
-			<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
-				<h5 class="section-heading">Notifications</h5>
-				<div class="gf-form-inline">
-					<div class="gf-form max-width-30">
-						<span class="gf-form-label width-8">Send to</span>
-						<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
-							<i class="{{nc.iconClass}}"></i>&nbsp;{{nc.name}}&nbsp;
-							<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
-						</span>
-						<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
-					</div>
-				</div>
-				<div class="gf-form gf-form--v-stretch">
-					<span class="gf-form-label width-8">Message</span>
-					<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message"  placeholder="Notification message details..."></textarea>
-				</div>
-			</div>
-
-			<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
-				<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i>&nbsp;Clear history</button>
-						<h5 class="section-heading" style="whitespace: nowrap">
-					State history <span class="muted small">(last 50 state changes)</span>
-				</h5>
-
-				<div ng-show="ctrl.alertHistory.length === 0">
-					<br>
-					<i>No state changes recorded</i>
-				</div>
-
-				<ol class="alert-rule-list" >
-					<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
-						<div class="alert-rule-item__icon {{al.stateModel.stateClass}}">
-							<i class="{{al.stateModel.iconClass}}"></i>
-						</div>
-						<div class="alert-rule-item__body">
-							<div class="alert-rule-item__header">
-								<div class="alert-rule-item__text">
-									<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
-								</div>
-							</div>
-							<span class="alert-list-info">{{al.info}}</span>
-						</div>
-						<div class="alert-rule-item__time">
-							<span>{{al.time}}</span>
-						</div>
-					</li>
-				</ol>
-			</div>
-		</div>
-	</div>
-</div>
-
-<div class="gf-form-group p-t-4 p-b-4" ng-if="!ctrl.alert">
-	<div class="empty-list-cta">
-		<div class="empty-list-cta__title">Panel has no alert rule defined</div>
-			<button class="empty-list-cta__button btn btn-xlarge btn-success" ng-click="ctrl.enable()">
-				<i class="icon-gf icon-gf-alert"></i>
-				Create Alert
-			</button>
-		</div>
-	</div>
+  <div class="panel-option-section">
+    <div class="panel-option-section__header">Notifications</div>
+    <div class="panel-option-section__body">
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label width-8">Send to</span>
+        </div>
+        <div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
+          <span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
+            <i class="{{nc.iconClass}}"></i>&nbsp;{{nc.name}}&nbsp;
+            <i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
+          </span>
+        </div>
+        <div class="gf-form">
+          <metric-segment segment="ctrl.addNotificationSegment"
+                          get-options="ctrl.getNotifications()"
+                          on-change="ctrl.notificationAdded()"></metric-segment>
+        </div>
+      </div>
+      <div class="gf-form gf-form--v-stretch">
+        <span class="gf-form-label width-8">Message</span>
+        <textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message"
+                  placeholder="Notification message details..."></textarea>
+      </div>
+    </div>
+  </div>
 </div>
 </div>

+ 80 - 11
public/app/features/dashboard/dashgrid/AlertTab.tsx

@@ -1,20 +1,30 @@
+// Libraries
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
-import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
-import { EditorTabBody } from './EditorTabBody';
+// Services & Utils
+import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
+import appEvents from 'app/core/app_events';
+
+// Components
+import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
+import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
+import StateHistory from './StateHistory';
 import 'app/features/alerting/AlertTabCtrl';
 import 'app/features/alerting/AlertTabCtrl';
 
 
+// Types
+import { DashboardModel } from '../dashboard_model';
+import { PanelModel } from '../panel_model';
+
 interface Props {
 interface Props {
   angularPanel?: AngularComponent;
   angularPanel?: AngularComponent;
+  dashboard: DashboardModel;
+  panel: PanelModel;
 }
 }
 
 
 export class AlertTab extends PureComponent<Props> {
 export class AlertTab extends PureComponent<Props> {
   element: any;
   element: any;
   component: AngularComponent;
   component: AngularComponent;
-
-  constructor(props) {
-    super(props);
-  }
+  panelCtrl: any;
 
 
   componentDidMount() {
   componentDidMount() {
     if (this.shouldLoadAlertTab()) {
     if (this.shouldLoadAlertTab()) {
@@ -29,7 +39,7 @@ export class AlertTab extends PureComponent<Props> {
   }
   }
 
 
   shouldLoadAlertTab() {
   shouldLoadAlertTab() {
-    return this.props.angularPanel && this.element;
+    return this.props.angularPanel && this.element && !this.component;
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
@@ -51,21 +61,80 @@ export class AlertTab extends PureComponent<Props> {
       return;
       return;
     }
     }
 
 
-    const panelCtrl = scope.$$childHead.ctrl;
+    this.panelCtrl = scope.$$childHead.ctrl;
     const loader = getAngularLoader();
     const loader = getAngularLoader();
     const template = '<alert-tab />';
     const template = '<alert-tab />';
 
 
     const scopeProps = {
     const scopeProps = {
-      ctrl: panelCtrl,
+      ctrl: this.panelCtrl,
     };
     };
 
 
     this.component = loader.load(this.element, scopeProps, template);
     this.component = loader.load(this.element, scopeProps, template);
   }
   }
 
 
+  stateHistory = (): EditorToolbarView => {
+    return {
+      title: 'State history',
+      render: () => {
+        return (
+          <StateHistory
+            dashboard={this.props.dashboard}
+            panelId={this.props.panel.id}
+            onRefresh={this.panelCtrl.refresh}
+          />
+        );
+      },
+    };
+  };
+
+  deleteAlert = (): EditorToolbarView => {
+    const { panel } = this.props;
+    return {
+      title: 'Delete',
+      btnType: 'danger',
+      onClick: () => {
+        appEvents.emit('confirm-modal', {
+          title: 'Delete Alert',
+          text: 'Are you sure you want to delete this alert rule?',
+          text2: 'You need to save dashboard for the delete to take effect',
+          icon: 'fa-trash',
+          yesText: 'Delete',
+          onConfirm: () => {
+            delete panel.alert;
+            panel.thresholds = [];
+            this.panelCtrl.alertState = null;
+            this.panelCtrl.render();
+            this.forceUpdate();
+          },
+        });
+      },
+    };
+  };
+
+  onAddAlert = () => {
+    this.panelCtrl._enableAlert();
+    this.component.digest();
+    this.forceUpdate();
+  };
+
   render() {
   render() {
+    const { alert } = this.props.panel;
+
+    const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : [];
+
+    const model = {
+      title: 'Panel has no alert rule defined',
+      icon: 'icon-gf icon-gf-alert',
+      onClick: this.onAddAlert,
+      buttonTitle: 'Create Alert',
+    };
+
     return (
     return (
-      <EditorTabBody heading="Alert" toolbarItems={[]}>
-        <div ref={element => (this.element = element)} />
+      <EditorTabBody heading="Alert" toolbarItems={toolbarItems}>
+        <>
+          <div ref={element => (this.element = element)} />
+          {!alert && <EmptyListCTA model={model} />}
+        </>
       </EditorTabBody>
       </EditorTabBody>
     );
     );
   }
   }

+ 13 - 9
public/app/features/dashboard/dashgrid/EditorTabBody.tsx

@@ -10,21 +10,22 @@ interface Props {
   children: JSX.Element;
   children: JSX.Element;
   heading: string;
   heading: string;
   renderToolbar?: () => JSX.Element;
   renderToolbar?: () => JSX.Element;
-  toolbarItems?: EditorToolBarView[];
+  toolbarItems?: EditorToolbarView[];
 }
 }
 
 
-export interface EditorToolBarView {
+export interface EditorToolbarView {
   title?: string;
   title?: string;
   heading?: string;
   heading?: string;
-  imgSrc?: string;
   icon?: string;
   icon?: string;
   disabled?: boolean;
   disabled?: boolean;
   onClick?: () => void;
   onClick?: () => void;
-  render: (closeFunction?: any) => JSX.Element | JSX.Element[];
+  render?: () => JSX.Element;
+  action?: () => void;
+  btnType?: 'danger';
 }
 }
 
 
 interface State {
 interface State {
-  openView?: EditorToolBarView;
+  openView?: EditorToolbarView;
   isOpen: boolean;
   isOpen: boolean;
   fadeIn: boolean;
   fadeIn: boolean;
 }
 }
@@ -48,7 +49,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
     this.setState({ fadeIn: true });
     this.setState({ fadeIn: true });
   }
   }
 
 
-  onToggleToolBarView = (item: EditorToolBarView) => {
+  onToggleToolBarView = (item: EditorToolbarView) => {
     this.setState({
     this.setState({
       openView: item,
       openView: item,
       isOpen: !this.state.isOpen,
       isOpen: !this.state.isOpen,
@@ -74,12 +75,15 @@ export class EditorTabBody extends PureComponent<Props, State> {
     return state;
     return state;
   }
   }
 
 
-  renderButton(view: EditorToolBarView) {
+  renderButton(view: EditorToolbarView) {
     const onClick = () => {
     const onClick = () => {
       if (view.onClick) {
       if (view.onClick) {
         view.onClick();
         view.onClick();
       }
       }
-      this.onToggleToolBarView(view);
+
+      if (view.render) {
+        this.onToggleToolBarView(view);
+      }
     };
     };
 
 
     return (
     return (
@@ -91,7 +95,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
     );
     );
   }
   }
 
 
-  renderOpenView(view: EditorToolBarView) {
+  renderOpenView(view: EditorToolbarView) {
     return (
     return (
       <PanelOptionSection title={view.title || view.heading} onClose={this.onCloseOpenView}>
       <PanelOptionSection title={view.title || view.heading} onClose={this.onCloseOpenView}>
         {view.render()}
         {view.render()}

+ 1 - 1
public/app/features/dashboard/dashgrid/PanelEditor.tsx

@@ -54,7 +54,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
       case 'queries':
       case 'queries':
         return <QueriesTab panel={panel} dashboard={dashboard} />;
         return <QueriesTab panel={panel} dashboard={dashboard} />;
       case 'alert':
       case 'alert':
-        return <AlertTab angularPanel={angularPanel} />;
+        return <AlertTab angularPanel={angularPanel} dashboard={dashboard} panel={panel} />;
       case 'visualization':
       case 'visualization':
         return (
         return (
           <VisualizationTab
           <VisualizationTab

+ 8 - 8
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -1,10 +1,10 @@
 // Libraries
 // Libraries
-import React, { SFC, PureComponent } from 'react';
+import React, { PureComponent, SFC } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 // Components
 // Components
-import './../../panel/metrics_tab';
-import { EditorTabBody } from './EditorTabBody';
+import 'app/features/panel/metrics_tab';
+import { EditorTabBody, EditorToolbarView} from './EditorTabBody';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { QueryInspector } from './QueryInspector';
 import { QueryInspector } from './QueryInspector';
 import { QueryOptions } from './QueryOptions';
 import { QueryOptions } from './QueryOptions';
@@ -13,14 +13,14 @@ import { PanelOptionSection } from './PanelOptionSection';
 
 
 // Services
 // Services
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
-import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
-import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
+import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
 import config from 'app/core/config';
 import config from 'app/core/config';
 
 
 // Types
 // Types
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';
 import { DashboardModel } from '../dashboard_model';
-import { DataSourceSelectItem, DataQuery } from 'app/types';
+import { DataQuery, DataSourceSelectItem } from 'app/types';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
 
 
 interface Props {
 interface Props {
@@ -204,12 +204,12 @@ export class QueriesTab extends PureComponent<Props, State> {
     const { panel } = this.props;
     const { panel } = this.props;
     const { currentDS, isAddingMixed } = this.state;
     const { currentDS, isAddingMixed } = this.state;
 
 
-    const queryInspector = {
+    const queryInspector: EditorToolbarView = {
       title: 'Query Inspector',
       title: 'Query Inspector',
       render: this.renderQueryInspector,
       render: this.renderQueryInspector,
     };
     };
 
 
-    const dsHelp = {
+    const dsHelp: EditorToolbarView = {
       heading: 'Help',
       heading: 'Help',
       icon: 'fa fa-question',
       icon: 'fa fa-question',
       render: this.renderHelp,
       render: this.renderHelp,

+ 110 - 0
public/app/features/dashboard/dashgrid/StateHistory.tsx

@@ -0,0 +1,110 @@
+import React, { PureComponent } from 'react';
+import alertDef from '../../alerting/state/alertDef';
+import { getBackendSrv } from 'app/core/services/backend_srv';
+import { DashboardModel } from '../dashboard_model';
+import appEvents from '../../../core/app_events';
+
+interface Props {
+  dashboard: DashboardModel;
+  panelId: number;
+  onRefresh: () => void;
+}
+
+interface State {
+  stateHistoryItems: any[];
+}
+
+class StateHistory extends PureComponent<Props, State> {
+  state = {
+    stateHistoryItems: [],
+  };
+
+  componentDidMount(): void {
+    const { dashboard, panelId } = this.props;
+
+    getBackendSrv()
+      .get(`/api/annotations?dashboardId=${dashboard.id}&panelId=${panelId}&limit=50&type=alert`)
+      .then(res => {
+        const items = res.map(item => {
+          return {
+            stateModel: alertDef.getStateDisplayModel(item.newState),
+            time: dashboard.formatDate(item.time, 'MMM D, YYYY HH:mm:ss'),
+            info: alertDef.getAlertAnnotationInfo(item),
+          };
+        });
+
+        this.setState({
+          stateHistoryItems: items,
+        });
+      });
+  }
+
+  clearHistory = () => {
+    const { dashboard, onRefresh, panelId } = this.props;
+
+    appEvents.emit('confirm-modal', {
+      title: 'Delete Alert History',
+      text: 'Are you sure you want to remove all history & annotations for this alert?',
+      icon: 'fa-trash',
+      yesText: 'Yes',
+      onConfirm: () => {
+        getBackendSrv()
+          .post('/api/annotations/mass-delete', {
+            dashboardId: dashboard.id,
+            panelId: panelId,
+          })
+          .then(() => {
+            onRefresh();
+          });
+
+        this.setState({
+          stateHistoryItems: [],
+        });
+      },
+    });
+  };
+
+  render() {
+    const { stateHistoryItems } = this.state;
+
+    return (
+      <div>
+        {stateHistoryItems.length > 0 && (
+          <div className="p-b-1">
+            <span className="muted">Last 50 state changes</span>
+            <button className="btn btn-mini btn-danger pull-right" onClick={this.clearHistory}>
+              <i className="fa fa-trash" /> {` Clear history`}
+            </button>
+          </div>
+        )}
+        <ol className="alert-rule-list">
+          {stateHistoryItems.length > 0 ? (
+            stateHistoryItems.map((item, index) => {
+              return (
+                <li className="alert-rule-item" key={`${item.time}-${index}`}>
+                  <div className={`alert-rule-item__icon ${item.stateModel.stateClass}`}>
+                    <i className={item.stateModel.iconClass} />
+                  </div>
+                  <div className="alert-rule-item__body">
+                    <div className="alert-rule-item__header">
+                      <p className="alert-rule-item__name">{item.alertName}</p>
+                      <div className="alert-rule-item__text">
+                        <span className={`${item.stateModel.stateClass}`}>{item.stateModel.text}</span>
+                      </div>
+                    </div>
+                    {item.info}
+                  </div>
+                  <div className="alert-rule-item__time">{item.time}</div>
+                </li>
+              );
+            })
+          ) : (
+            <i>No state changes recorded</i>
+          )}
+        </ol>
+      </div>
+    );
+  }
+}
+
+export default StateHistory;

+ 3 - 3
public/app/features/dashboard/dashgrid/VisualizationTab.tsx

@@ -2,10 +2,10 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
 // Utils & Services
 // Utils & Services
-import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
 
 
 // Components
 // Components
-import { EditorTabBody } from './EditorTabBody';
+import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
 import { VizTypePicker } from './VizTypePicker';
 import { VizTypePicker } from './VizTypePicker';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
 import { FadeIn } from 'app/core/components/Animations/FadeIn';
 import { FadeIn } from 'app/core/components/Animations/FadeIn';
@@ -206,7 +206,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
     const { plugin } = this.props;
     const { plugin } = this.props;
     const { isVizPickerOpen, searchQuery } = this.state;
     const { isVizPickerOpen, searchQuery } = this.state;
 
 
-    const pluginHelp = {
+    const pluginHelp: EditorToolbarView = {
       heading: 'Help',
       heading: 'Help',
       icon: 'fa fa-question',
       icon: 'fa fa-question',
       render: this.renderHelp,
       render: this.renderHelp,

+ 1 - 1
public/app/features/datasources/settings/BasicSettings.tsx

@@ -16,7 +16,7 @@ const BasicSettings: SFC<Props> = ({ dataSourceName, isDefault, onDefaultChange,
         <div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
         <div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
           <Label
           <Label
             tooltip={
             tooltip={
-              'The name is used when you select the data source in panels. The Default data source is' +
+              'The name is used when you select the data source in panels. The Default data source is ' +
               'preselected in new panels.'
               'preselected in new panels.'
             }
             }
           >
           >

+ 1 - 1
public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap

@@ -16,7 +16,7 @@ exports[`Render should render component 1`] = `
       }
       }
     >
     >
       <Component
       <Component
-        tooltip="The name is used when you select the data source in panels. The Default data source ispreselected in new panels."
+        tooltip="The name is used when you select the data source in panels. The Default data source is preselected in new panels."
       >
       >
         Name
         Name
       </Component>
       </Component>

+ 1 - 1
public/app/features/datasources/state/reducers.ts

@@ -5,7 +5,7 @@ import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelec
 const initialState: DataSourcesState = {
 const initialState: DataSourcesState = {
   dataSources: [] as DataSource[],
   dataSources: [] as DataSource[],
   dataSource: {} as DataSource,
   dataSource: {} as DataSource,
-  layoutMode: LayoutModes.Grid,
+  layoutMode: LayoutModes.List,
   searchQuery: '',
   searchQuery: '',
   dataSourcesCount: 0,
   dataSourcesCount: 0,
   dataSourceTypes: [] as Plugin[],
   dataSourceTypes: [] as Plugin[],

+ 15 - 6
public/app/features/explore/Explore.tsx

@@ -40,6 +40,8 @@ import ErrorBoundary from './ErrorBoundary';
 import { Alert } from './Error';
 import { Alert } from './Error';
 import TimePicker, { parseTime } from './TimePicker';
 import TimePicker, { parseTime } from './TimePicker';
 
 
+const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
+
 interface ExploreProps {
 interface ExploreProps {
   datasourceSrv: DatasourceSrv;
   datasourceSrv: DatasourceSrv;
   onChangeSplit: (split: boolean, state?: ExploreState) => void;
   onChangeSplit: (split: boolean, state?: ExploreState) => void;
@@ -90,6 +92,10 @@ interface ExploreProps {
 export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
 export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   el: any;
   el: any;
   exploreEvents: Emitter;
   exploreEvents: Emitter;
+  /**
+   * Set via URL or local storage
+   */
+  initialDatasource: string;
   /**
   /**
    * Current query expressions of the rows including their modifications, used for running queries.
    * Current query expressions of the rows including their modifications, used for running queries.
    * Not kept in component state to prevent edit-render roundtrips.
    * Not kept in component state to prevent edit-render roundtrips.
@@ -115,6 +121,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       initialQueries = splitState.initialQueries;
       initialQueries = splitState.initialQueries;
     } else {
     } else {
       const { datasource, queries, range } = props.urlState as ExploreUrlState;
       const { datasource, queries, range } = props.urlState as ExploreUrlState;
+      const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
       initialQueries = ensureQueries(queries);
       initialQueries = ensureQueries(queries);
       const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE };
       const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE };
       // Millies step for helper bar charts
       // Millies step for helper bar charts
@@ -124,10 +131,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         datasourceError: null,
         datasourceError: null,
         datasourceLoading: null,
         datasourceLoading: null,
         datasourceMissing: false,
         datasourceMissing: false,
-        datasourceName: datasource,
         exploreDatasources: [],
         exploreDatasources: [],
         graphInterval: initialGraphInterval,
         graphInterval: initialGraphInterval,
         graphResult: [],
         graphResult: [],
+        initialDatasource,
         initialQueries,
         initialQueries,
         history: [],
         history: [],
         logsResult: null,
         logsResult: null,
@@ -151,7 +158,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
 
 
   async componentDidMount() {
   async componentDidMount() {
     const { datasourceSrv } = this.props;
     const { datasourceSrv } = this.props;
-    const { datasourceName } = this.state;
+    const { initialDatasource } = this.state;
     if (!datasourceSrv) {
     if (!datasourceSrv) {
       throw new Error('No datasource service passed as props.');
       throw new Error('No datasource service passed as props.');
     }
     }
@@ -165,10 +172,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
 
 
     if (datasources.length > 0) {
     if (datasources.length > 0) {
       this.setState({ datasourceLoading: true, exploreDatasources });
       this.setState({ datasourceLoading: true, exploreDatasources });
-      // Priority: datasource in url, default datasource, first explore datasource
+      // Priority for datasource preselection: URL, localstorage, default datasource
       let datasource;
       let datasource;
-      if (datasourceName) {
-        datasource = await datasourceSrv.get(datasourceName);
+      if (initialDatasource) {
+        datasource = await datasourceSrv.get(initialDatasource);
       } else {
       } else {
         datasource = await datasourceSrv.get();
         datasource = await datasourceSrv.get();
       }
       }
@@ -253,13 +260,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         supportsLogs,
         supportsLogs,
         supportsTable,
         supportsTable,
         datasourceLoading: false,
         datasourceLoading: false,
-        datasourceName: datasource.name,
+        initialDatasource: datasource.name,
         initialQueries: nextQueries,
         initialQueries: nextQueries,
         logsHighlighterExpressions: undefined,
         logsHighlighterExpressions: undefined,
         showingStartPage: Boolean(StartPage),
         showingStartPage: Boolean(StartPage),
       },
       },
       () => {
       () => {
         if (datasourceError === null) {
         if (datasourceError === null) {
+          // Save last-used datasource
+          store.set(LAST_USED_DATASOURCE_KEY, datasource.name);
           this.onSubmit();
           this.onSubmit();
         }
         }
       }
       }

+ 0 - 1
public/app/features/explore/LogLabels.tsx

@@ -1,4 +1,3 @@
-import _ from 'lodash';
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import classnames from 'classnames';
 import classnames from 'classnames';
 
 

+ 1 - 1
public/app/features/explore/Typeahead.tsx

@@ -55,7 +55,7 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps> {
 interface TypeaheadGroupProps {
 interface TypeaheadGroupProps {
   items: CompletionItem[];
   items: CompletionItem[];
   label: string;
   label: string;
-  onClickItem: (CompletionItem) => void;
+  onClickItem: (suggestion: CompletionItem) => void;
   selected: CompletionItem;
   selected: CompletionItem;
   prefix?: string;
   prefix?: string;
 }
 }

+ 0 - 3
public/app/features/panel/metrics_tab.ts

@@ -1,6 +1,3 @@
-// Libraries
-import _ from 'lodash';
-
 // Services & utils
 // Services & utils
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import { Emitter } from 'app/core/utils/emitter';
 import { Emitter } from 'app/core/utils/emitter';

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

@@ -308,7 +308,7 @@ export default class InfluxDatasource {
         return 'now()';
         return 'now()';
       }
       }
 
 
-      const parts = /^now-(\d+)([d|h|m|s])$/.exec(date);
+      const parts = /^now-(\d+)([dhms])$/.exec(date);
       if (parts) {
       if (parts) {
         const amount = parseInt(parts[1], 10);
         const amount = parseInt(parts[1], 10);
         const unit = parts[2];
         const unit = parts[2];

+ 0 - 1
public/app/plugins/datasource/loki/components/LokiQueryField.tsx

@@ -1,4 +1,3 @@
-import _ from 'lodash';
 import React from 'react';
 import React from 'react';
 import Cascader from 'rc-cascader';
 import Cascader from 'rc-cascader';
 import PluginPrism from 'slate-prism';
 import PluginPrism from 'slate-prism';

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

@@ -16,7 +16,6 @@ export default class StackdriverDatasource {
   constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
   constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
     this.baseUrl = `/stackdriver/`;
     this.baseUrl = `/stackdriver/`;
     this.url = instanceSettings.url;
     this.url = instanceSettings.url;
-    this.doRequest = this.doRequest;
     this.id = instanceSettings.id;
     this.id = instanceSettings.id;
     this.projectName = instanceSettings.jsonData.defaultProject || '';
     this.projectName = instanceSettings.jsonData.defaultProject || '';
     this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
     this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';

+ 2 - 2
public/app/plugins/panel/gauge/module.tsx

@@ -38,8 +38,8 @@ export const defaultProps = {
     showThresholdLabels: false,
     showThresholdLabels: false,
     suffix: '',
     suffix: '',
     decimals: 0,
     decimals: 0,
-    stat: '',
-    unit: '',
+    stat: 'avg',
+    unit: 'none',
     mappings: [],
     mappings: [],
     thresholds: [],
     thresholds: [],
   },
   },

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

@@ -251,7 +251,6 @@ export class HeatmapRenderer {
     if (tickInterval === 0) {
     if (tickInterval === 0) {
       yMax = max * this.dataRangeWidingFactor;
       yMax = max * this.dataRangeWidingFactor;
       yMin = min - min * (this.dataRangeWidingFactor - 1);
       yMin = min - min * (this.dataRangeWidingFactor - 1);
-      tickInterval = (yMax - yMin) / 2;
     } else {
     } else {
       yMax = Math.ceil((max + yWiding) / tickInterval) * tickInterval;
       yMax = Math.ceil((max + yWiding) / tickInterval) * tickInterval;
       yMin = Math.floor((min - yWiding) / tickInterval) * tickInterval;
       yMin = Math.floor((min - yWiding) / tickInterval) * tickInterval;
@@ -389,9 +388,7 @@ export class HeatmapRenderer {
 
 
   // Adjust data range to log base
   // Adjust data range to log base
   adjustLogRange(min, max, logBase) {
   adjustLogRange(min, max, logBase) {
-    let yMin, yMax;
-
-    yMin = this.data.heatmapStats.minLog;
+    let yMin = this.data.heatmapStats.minLog;
     if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
     if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
       yMin = 1;
       yMin = 1;
     } else {
     } else {
@@ -399,7 +396,7 @@ export class HeatmapRenderer {
     }
     }
 
 
     // Adjust max Y value to log base
     // Adjust max Y value to log base
-    yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase);
+    const yMax = this.adjustLogMax(this.data.heatmapStats.max, logBase);
 
 
     return { yMin, yMax };
     return { yMin, yMax };
   }
   }

+ 1 - 1
public/app/types/explore.ts

@@ -155,11 +155,11 @@ export interface ExploreState {
   datasourceError: any;
   datasourceError: any;
   datasourceLoading: boolean | null;
   datasourceLoading: boolean | null;
   datasourceMissing: boolean;
   datasourceMissing: boolean;
-  datasourceName?: string;
   exploreDatasources: DataSourceSelectItem[];
   exploreDatasources: DataSourceSelectItem[];
   graphInterval: number; // in ms
   graphInterval: number; // in ms
   graphResult?: any[];
   graphResult?: any[];
   history: HistoryItem[];
   history: HistoryItem[];
+  initialDatasource?: string;
   initialQueries: DataQuery[];
   initialQueries: DataQuery[];
   logsHighlighterExpressions?: string[];
   logsHighlighterExpressions?: string[];
   logsResult?: LogsModel;
   logsResult?: LogsModel;

+ 55 - 0
public/app/viz/Gauge.test.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { Gauge, Props } from './Gauge';
+import { BasicGaugeColor } from '../types';
+import { TimeSeriesVMs } from '@grafana/ui';
+
+jest.mock('jquery', () => ({
+  plot: jest.fn(),
+}));
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    baseColor: BasicGaugeColor.Green,
+    maxValue: 100,
+    mappings: [],
+    minValue: 0,
+    prefix: '',
+    showThresholdMarkers: true,
+    showThresholdLabels: false,
+    suffix: '',
+    thresholds: [],
+    unit: 'none',
+    stat: 'avg',
+    height: 300,
+    width: 300,
+    timeSeries: {} as TimeSeriesVMs,
+    decimals: 0,
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<Gauge {...props} />);
+  const instance = wrapper.instance() as Gauge;
+
+  return {
+    instance,
+    wrapper,
+  };
+};
+
+describe('Get font color', () => {
+  it('should get base color if no threshold', () => {
+    const { instance } = setup();
+
+    expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green);
+  });
+
+  it('should be f2f2f2', () => {
+    const { instance } = setup({
+      thresholds: [{ value: 59, color: '#f2f2f2' }],
+    });
+
+    expect(instance.getFontColor(58)).toEqual('#f2f2f2');
+  });
+});

+ 8 - 6
public/app/viz/Gauge.tsx

@@ -5,7 +5,7 @@ import { TimeSeriesVMs } from '@grafana/ui';
 import config from '../core/config';
 import config from '../core/config';
 import kbn from '../core/utils/kbn';
 import kbn from '../core/utils/kbn';
 
 
-interface Props {
+export interface Props {
   baseColor: string;
   baseColor: string;
   decimals: number;
   decimals: number;
   height: number;
   height: number;
@@ -96,12 +96,14 @@ export class Gauge extends PureComponent<Props> {
   getFontColor(value) {
   getFontColor(value) {
     const { baseColor, maxValue, thresholds } = this.props;
     const { baseColor, maxValue, thresholds } = this.props;
 
 
-    const atThreshold = thresholds.filter(threshold => value <= threshold.value);
+    if (thresholds.length > 0) {
+      const atThreshold = thresholds.filter(threshold => value <= threshold.value);
 
 
-    if (atThreshold.length > 0) {
-      return atThreshold[0].color;
-    } else if (value <= maxValue) {
-      return BasicGaugeColor.Red;
+      if (atThreshold.length > 0) {
+        return atThreshold[0].color;
+      } else if (value <= maxValue) {
+        return BasicGaugeColor.Red;
+      }
     }
     }
 
 
     return baseColor;
     return baseColor;

+ 1 - 1
public/emails/invited_to_org.html

@@ -1,5 +1,5 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
+<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
 <head>
 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 	<meta name="viewport" content="width=device-width" />
 	<meta name="viewport" content="width=device-width" />

+ 1 - 1
public/emails/new_user_invite.html

@@ -1,5 +1,5 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml">
+<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
 <head>
 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 	<meta name="viewport" content="width=device-width" />
 	<meta name="viewport" content="width=device-width" />

+ 1 - 0
public/sass/components/_form_select_box.scss

@@ -63,6 +63,7 @@ $select-input-bg-disabled: $input-bg-disabled;
 .gf-form-select-box__menu-list {
 .gf-form-select-box__menu-list {
   overflow-y: auto;
   overflow-y: auto;
   max-height: 300px;
   max-height: 300px;
+  max-width: 600px;
 }
 }
 
 
 .tag-filter .gf-form-select-box__menu {
 .tag-filter .gf-form-select-box__menu {

+ 2 - 0
public/sass/components/_timepicker.scss

@@ -23,7 +23,9 @@
   left: 20px;
   left: 20px;
   right: 20px;
   right: 20px;
   top: $navbarHeight;
   top: $navbarHeight;
+
   @include media-breakpoint-up(md) {
   @include media-breakpoint-up(md) {
+    left: auto;
     width: 550px;
     width: 550px;
   }
   }
 
 

+ 1 - 0
public/sass/pages/_alerting.scss

@@ -107,6 +107,7 @@
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   flex-grow: 1;
   flex-grow: 1;
+  justify-content: center;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 

+ 1 - 3
scripts/webpack/webpack.common.js

@@ -3,9 +3,6 @@ const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
 
 
 module.exports = {
 module.exports = {
   target: 'web',
   target: 'web',
-  stats: {
-    children: false
-  },
   entry: {
   entry: {
     app: './public/app/index.ts',
     app: './public/app/index.ts',
   },
   },
@@ -25,6 +22,7 @@ module.exports = {
     ],
     ],
   },
   },
   stats: {
   stats: {
+    children: false,
     warningsFilter: /export .* was not found in/
     warningsFilter: /export .* was not found in/
   },
   },
   node: {
   node: {

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

@@ -3,7 +3,6 @@
 const merge = require('webpack-merge');
 const merge = require('webpack-merge');
 const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 const common = require('./webpack.common.js');
 const common = require('./webpack.common.js');
-const webpack = require('webpack');
 const path = require('path');
 const path = require('path');
 const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
 const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const HtmlWebpackPlugin = require("html-webpack-plugin");

+ 4 - 26
yarn.lock

@@ -3969,7 +3969,7 @@ debug@^4.1.0:
   dependencies:
   dependencies:
     ms "^2.1.1"
     ms "^2.1.1"
 
 
-debuglog@*, debuglog@^1.0.1:
+debuglog@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
 
 
@@ -6327,7 +6327,7 @@ import-local@^2.0.0:
     pkg-dir "^3.0.0"
     pkg-dir "^3.0.0"
     resolve-cwd "^2.0.0"
     resolve-cwd "^2.0.0"
 
 
-imurmurhash@*, imurmurhash@^0.1.4:
+imurmurhash@^0.1.4:
   version "0.1.4"
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
 
 
@@ -7809,10 +7809,6 @@ lockfile@^1.0.4:
   dependencies:
   dependencies:
     signal-exit "^3.0.2"
     signal-exit "^3.0.2"
 
 
-lodash._baseindexof@*:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
-
 lodash._baseuniq@~4.6.0:
 lodash._baseuniq@~4.6.0:
   version "4.6.0"
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
   resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -7820,25 +7816,11 @@ lodash._baseuniq@~4.6.0:
     lodash._createset "~4.0.0"
     lodash._createset "~4.0.0"
     lodash._root "~3.0.0"
     lodash._root "~3.0.0"
 
 
-lodash._bindcallback@*:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
-
-lodash._cacheindexof@*:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
-
-lodash._createcache@*:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
-  dependencies:
-    lodash._getnative "^3.0.0"
-
 lodash._createset@~4.0.0:
 lodash._createset@~4.0.0:
   version "4.0.3"
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
   resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
 
 
-lodash._getnative@*, lodash._getnative@^3.0.0:
+lodash._getnative@^3.0.0:
   version "3.9.1"
   version "3.9.1"
   resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
   resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
 
 
@@ -7914,10 +7896,6 @@ lodash.mergewith@^4.6.0:
   version "4.6.1"
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
   resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
 
 
-lodash.restparam@*:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
-
 lodash.sortby@^4.7.0:
 lodash.sortby@^4.7.0:
   version "4.7.0"
   version "4.7.0"
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -10742,7 +10720,7 @@ readable-stream@~1.1.10:
     isarray "0.0.1"
     isarray "0.0.1"
     string_decoder "~0.10.x"
     string_decoder "~0.10.x"
 
 
-readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
+readdir-scoped-modules@^1.0.0:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
   resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
   dependencies:
   dependencies: