Ver Fonte

Alert panel filters (#11712)

alert list panel: filter alerts by name, dashboard, folder, tags
Patrick O'Carroll há 7 anos atrás
pai
commit
0c269d64d0

+ 8 - 3
docs/sources/http_api/alerting.md

@@ -35,10 +35,15 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
   `/api/alerts?dashboardId=1`
 
-  - **dashboardId** – Return alerts for a specified dashboard.
-  - **panelId** – Return alerts for a specified panel on a dashboard.
-  - **limit** - Limit response to x number of alerts.
+  - **dashboardId** – Limit response to alerts in specified dashboard(s). You can specify multiple dashboards, e.g. dashboardId=23&dashboardId=35.
+  - **panelId** – Limit response to alert for a specified panel on a dashboard.
+  - **query** - Limit response to alerts having a name like this value.
   - **state** - Return alerts with one or more of the following alert states: `ALL`,`no_data`, `paused`, `alerting`, `ok`, `pending`. To specify multiple states use the following format: `?state=paused&state=alerting`
+  - **limit** - Limit response to *X* number of alerts.
+  - **folderId** – Limit response to alerts of dashboards in specified folder(s). You can specify multiple folders, e.g. folderId=23&folderId=35.
+  - **dashboardQuery** - Limit response to alerts having a dashboard name like this value.
+  - **dashboardTag** - Limit response to alerts of dashboards with specified tags. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. dashboardTag=tag1&dashboardTag=tag2.
+
 
 **Example Response**:
 

+ 59 - 5
pkg/api/alerting.go

@@ -2,12 +2,14 @@ package api
 
 import (
 	"fmt"
+	"strconv"
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/guardian"
+	"github.com/grafana/grafana/pkg/services/search"
 )
 
 func ValidateOrgAlert(c *m.ReqContext) {
@@ -46,12 +48,64 @@ func GetAlertStatesForDashboard(c *m.ReqContext) Response {
 
 // GET /api/alerts
 func GetAlerts(c *m.ReqContext) Response {
+	dashboardQuery := c.Query("dashboardQuery")
+	dashboardTags := c.QueryStrings("dashboardTag")
+	stringDashboardIDs := c.QueryStrings("dashboardId")
+	stringFolderIDs := c.QueryStrings("folderId")
+
+	dashboardIDs := make([]int64, 0)
+	for _, id := range stringDashboardIDs {
+		dashboardID, err := strconv.ParseInt(id, 10, 64)
+		if err == nil {
+			dashboardIDs = append(dashboardIDs, dashboardID)
+		}
+	}
+
+	if dashboardQuery != "" || len(dashboardTags) > 0 || len(stringFolderIDs) > 0 {
+		folderIDs := make([]int64, 0)
+		for _, id := range stringFolderIDs {
+			folderID, err := strconv.ParseInt(id, 10, 64)
+			if err == nil {
+				folderIDs = append(folderIDs, folderID)
+			}
+		}
+
+		searchQuery := search.Query{
+			Title:        dashboardQuery,
+			Tags:         dashboardTags,
+			SignedInUser: c.SignedInUser,
+			Limit:        1000,
+			OrgId:        c.OrgId,
+			DashboardIds: dashboardIDs,
+			Type:         string(search.DashHitDB),
+			FolderIds:    folderIDs,
+			Permission:   m.PERMISSION_EDIT,
+		}
+
+		err := bus.Dispatch(&searchQuery)
+		if err != nil {
+			return Error(500, "List alerts failed", err)
+		}
+
+		for _, d := range searchQuery.Result {
+			if d.Type == search.DashHitDB && d.Id > 0 {
+				dashboardIDs = append(dashboardIDs, d.Id)
+			}
+		}
+
+		// if we didn't find any dashboards, return empty result
+		if len(dashboardIDs) == 0 {
+			return JSON(200, []*m.AlertListItemDTO{})
+		}
+	}
+
 	query := m.GetAlertsQuery{
-		OrgId:       c.OrgId,
-		DashboardId: c.QueryInt64("dashboardId"),
-		PanelId:     c.QueryInt64("panelId"),
-		Limit:       c.QueryInt64("limit"),
-		User:        c.SignedInUser,
+		OrgId:        c.OrgId,
+		DashboardIDs: dashboardIDs,
+		PanelId:      c.QueryInt64("panelId"),
+		Limit:        c.QueryInt64("limit"),
+		User:         c.SignedInUser,
+		Query:        c.Query("query"),
 	}
 
 	states := c.QueryStrings("state")

+ 55 - 0
pkg/api/alerting_test.go

@@ -6,6 +6,7 @@ import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/search"
 
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -64,6 +65,60 @@ func TestAlertingApiEndpoint(t *testing.T) {
 				})
 			})
 		})
+
+		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) {
+			var searchQuery *search.Query
+			bus.AddHandler("test", func(query *search.Query) error {
+				searchQuery = query
+				return nil
+			})
+
+			var getAlertsQuery *m.GetAlertsQuery
+			bus.AddHandler("test", func(query *m.GetAlertsQuery) error {
+				getAlertsQuery = query
+				return nil
+			})
+
+			sc.handlerFunc = GetAlerts
+			sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+			So(searchQuery, ShouldBeNil)
+			So(getAlertsQuery, ShouldNotBeNil)
+		})
+
+		loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) {
+			var searchQuery *search.Query
+			bus.AddHandler("test", func(query *search.Query) error {
+				searchQuery = query
+				query.Result = search.HitList{
+					&search.Hit{Id: 1},
+					&search.Hit{Id: 2},
+				}
+				return nil
+			})
+
+			var getAlertsQuery *m.GetAlertsQuery
+			bus.AddHandler("test", func(query *m.GetAlertsQuery) error {
+				getAlertsQuery = query
+				return nil
+			})
+
+			sc.handlerFunc = GetAlerts
+			sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+			So(searchQuery, ShouldNotBeNil)
+			So(searchQuery.DashboardIds[0], ShouldEqual, 1)
+			So(searchQuery.DashboardIds[1], ShouldEqual, 2)
+			So(searchQuery.FolderIds[0], ShouldEqual, 3)
+			So(searchQuery.Tags[0], ShouldEqual, "abc")
+			So(searchQuery.Title, ShouldEqual, "dbQuery")
+
+			So(getAlertsQuery, ShouldNotBeNil)
+			So(getAlertsQuery.DashboardIDs[0], ShouldEqual, 1)
+			So(getAlertsQuery.DashboardIDs[1], ShouldEqual, 2)
+			So(getAlertsQuery.Limit, ShouldEqual, 5)
+			So(getAlertsQuery.Query, ShouldEqual, "alertQuery")
+		})
 	})
 }
 

+ 7 - 6
pkg/models/alert.go

@@ -161,12 +161,13 @@ type SetAlertStateCommand struct {
 
 //Queries
 type GetAlertsQuery struct {
-	OrgId       int64
-	State       []string
-	DashboardId int64
-	PanelId     int64
-	Limit       int64
-	User        *SignedInUser
+	OrgId        int64
+	State        []string
+	DashboardIDs []int64
+	PanelId      int64
+	Limit        int64
+	Query        string
+	User         *SignedInUser
 
 	Result []*AlertListItemDTO
 }

+ 10 - 2
pkg/services/sqlstore/alert.go

@@ -82,8 +82,16 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 
 	builder.Write(`WHERE alert.org_id = ?`, query.OrgId)
 
-	if query.DashboardId != 0 {
-		builder.Write(` AND alert.dashboard_id = ?`, query.DashboardId)
+	if len(strings.TrimSpace(query.Query)) > 0 {
+		builder.Write(" AND alert.name "+dialect.LikeStr()+" ?", "%"+query.Query+"%")
+	}
+
+	if len(query.DashboardIDs) > 0 {
+		builder.sql.WriteString(` AND alert.dashboard_id IN (?` + strings.Repeat(",?", len(query.DashboardIDs)-1) + `) `)
+
+		for _, dbID := range query.DashboardIDs {
+			builder.AddParams(dbID)
+		}
 	}
 
 	if query.PanelId != 0 {

+ 8 - 7
pkg/services/sqlstore/alert_test.go

@@ -3,10 +3,11 @@ package sqlstore
 import (
 	"testing"
 
+	"time"
+
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
-	"time"
 )
 
 func mockTimeNow() {
@@ -99,7 +100,7 @@ func TestAlertingDataAccess(t *testing.T) {
 		})
 
 		Convey("Can read properties", func() {
-			alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
+			alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 			err2 := HandleAlertsQuery(&alertQuery)
 
 			alert := alertQuery.Result[0]
@@ -109,7 +110,7 @@ func TestAlertingDataAccess(t *testing.T) {
 		})
 
 		Convey("Viewer cannot read alerts", func() {
-			alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}}
+			alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}}
 			err2 := HandleAlertsQuery(&alertQuery)
 
 			So(err2, ShouldBeNil)
@@ -134,7 +135,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			})
 
 			Convey("Alerts should be updated", func() {
-				query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
+				query := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 				err2 := HandleAlertsQuery(&query)
 
 				So(err2, ShouldBeNil)
@@ -183,7 +184,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			Convey("Should save 3 dashboards", func() {
 				So(err, ShouldBeNil)
 
-				queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
+				queryForDashboard := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 				err2 := HandleAlertsQuery(&queryForDashboard)
 
 				So(err2, ShouldBeNil)
@@ -197,7 +198,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				err = SaveAlerts(&cmd)
 
 				Convey("should delete the missing alert", func() {
-					query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
+					query := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 					err2 := HandleAlertsQuery(&query)
 					So(err2, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 2)
@@ -232,7 +233,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			Convey("Alerts should be removed", func() {
-				query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
+				query := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
 				err2 := HandleAlertsQuery(&query)
 
 				So(testDash.Id, ShouldEqual, 1)

+ 10 - 2
public/app/features/dashboard/folder_picker/folder_picker.ts

@@ -12,6 +12,7 @@ export class FolderPickerCtrl {
   enterFolderCreation: any;
   exitFolderCreation: any;
   enableCreateNew: boolean;
+  enableReset: boolean;
   rootName = 'General';
   folder: any;
   createNewFolder: boolean;
@@ -58,6 +59,10 @@ export class FolderPickerCtrl {
         result.unshift({ title: '-- New Folder --', id: -1 });
       }
 
+      if (this.enableReset && query === '' && this.initialTitle !== '') {
+        result.unshift({ title: this.initialTitle, id: null });
+      }
+
       return _.map(result, item => {
         return { text: item.title, value: item.id };
       });
@@ -65,7 +70,9 @@ export class FolderPickerCtrl {
   }
 
   onFolderChange(option) {
-    if (option.value === -1) {
+    if (!option) {
+      option = { value: 0, text: this.rootName };
+    } else if (option.value === -1) {
       this.createNewFolder = true;
       this.enterFolderCreation();
       return;
@@ -134,7 +141,7 @@ export class FolderPickerCtrl {
         this.onFolderLoad();
       });
     } else {
-      if (this.initialTitle) {
+      if (this.initialTitle && this.initialFolderId === null) {
         this.folder = { text: this.initialTitle, value: null };
       } else {
         this.folder = { text: this.rootName, value: 0 };
@@ -171,6 +178,7 @@ export function folderPicker() {
       enterFolderCreation: '&',
       exitFolderCreation: '&',
       enableCreateNew: '@',
+      enableReset: '@',
     },
   };
 }

+ 24 - 0
public/app/plugins/panel/alertlist/editor.html

@@ -19,6 +19,30 @@
     </div>
     <gf-form-switch class="gf-form" label="Alerts from this dashboard" label-class="width-18" checked="ctrl.panel.onlyAlertsOnDashboard" on-change="ctrl.updateStateFilter()"></gf-form-switch>
   </div>
+  <div class="section gf-form-group" ng-show="ctrl.panel.show === 'current'">
+    <h5 class="section-heading">Filter</h5>
+    <div class="gf-form">
+      <span class="gf-form-label width-8">Alert name</span>
+      <input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.nameFilter" placeholder="Alert name query" ng-change="ctrl.onRefresh()" />
+    </div>
+    <div class="gf-form">
+      <span class="gf-form-label width-8">Dashboard title</span>
+      <input type="text" class="gf-form-input" placeholder="Dashboard title query" ng-model="ctrl.panel.dashboardFilter" ng-change="ctrl.onRefresh()" ng-model-onblur>
+    </div>
+    <div class="gf-form">
+      <folder-picker  initial-folder-id="ctrl.panel.folderId"
+                      on-change="ctrl.onFolderChange($folder)"
+                      label-class="width-8"
+                      initial-title="'All'"
+                      enable-reset="true" >
+      </folder-picker>
+    </div>
+    <div class="gf-form">
+        <span class="gf-form-label width-8">Dashboard tags</span>
+        <bootstrap-tagsinput ng-model="ctrl.panel.dashboardTags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
+        </bootstrap-tagsinput>
+    </div>
+  </div>
   <div class="section gf-form-group" ng-show="ctrl.panel.show === 'current'">
     <h5 class="section-heading">State filter</h5>
     <gf-form-switch class="gf-form" label="Ok" label-class="width-10" checked="ctrl.stateFilter['ok']" on-change="ctrl.updateStateFilter()"></gf-form-switch>

+ 29 - 0
public/app/plugins/panel/alertlist/module.ts

@@ -21,6 +21,7 @@ class AlertListPanel extends PanelCtrl {
   currentAlerts: any = [];
   alertHistory: any = [];
   noAlertsMessage: string;
+
   // Set and populate defaults
   panelDefaults = {
     show: 'current',
@@ -28,6 +29,9 @@ class AlertListPanel extends PanelCtrl {
     stateFilter: [],
     onlyAlertsOnDashboard: false,
     sortOrder: 1,
+    dashboardFilter: '',
+    nameFilter: '',
+    folderId: null,
   };
 
   /** @ngInject */
@@ -89,6 +93,11 @@ class AlertListPanel extends PanelCtrl {
     });
   }
 
+  onFolderChange(folder: any) {
+    this.panel.folderId = folder.id;
+    this.refresh();
+  }
+
   getStateChanges() {
     var params: any = {
       limit: this.panel.limit,
@@ -110,6 +119,7 @@ class AlertListPanel extends PanelCtrl {
         al.info = alertDef.getAlertAnnotationInfo(al);
         return al;
       });
+
       this.noAlertsMessage = this.alertHistory.length === 0 ? 'No alerts in current time range' : '';
 
       return this.alertHistory;
@@ -121,10 +131,26 @@ class AlertListPanel extends PanelCtrl {
       state: this.panel.stateFilter,
     };
 
+    if (this.panel.nameFilter) {
+      params.query = this.panel.nameFilter;
+    }
+
+    if (this.panel.folderId >= 0) {
+      params.folderId = this.panel.folderId;
+    }
+
+    if (this.panel.dashboardFilter) {
+      params.dashboardQuery = this.panel.dashboardFilter;
+    }
+
     if (this.panel.onlyAlertsOnDashboard) {
       params.dashboardId = this.dashboard.id;
     }
 
+    if (this.panel.dashboardTags) {
+      params.dashboardTag = this.panel.dashboardTags;
+    }
+
     return this.backendSrv.get(`/api/alerts`, params).then(res => {
       this.currentAlerts = this.sortResult(
         _.map(res, al => {
@@ -135,6 +161,9 @@ class AlertListPanel extends PanelCtrl {
           return al;
         })
       );
+      if (this.currentAlerts.length > this.panel.limit) {
+        this.currentAlerts = this.currentAlerts.slice(0, this.panel.limit);
+      }
       this.noAlertsMessage = this.currentAlerts.length === 0 ? 'No alerts' : '';
 
       return this.currentAlerts;