mirror of https://github.com/grafana/grafana.git
				
				
				
			Alert panel filters (#11712)
alert list panel: filter alerts by name, dashboard, folder, tags
This commit is contained in:
		
							parent
							
								
									13a9701581
								
							
						
					
					
						commit
						0c269d64d0
					
				|  | @ -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**: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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"), | ||||
| 		DashboardIDs: dashboardIDs, | ||||
| 		PanelId:      c.QueryInt64("panelId"), | ||||
| 		Limit:        c.QueryInt64("limit"), | ||||
| 		User:         c.SignedInUser, | ||||
| 		Query:        c.Query("query"), | ||||
| 	} | ||||
| 
 | ||||
| 	states := c.QueryStrings("state") | ||||
|  |  | |||
|  | @ -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") | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -163,9 +163,10 @@ type SetAlertStateCommand struct { | |||
| type GetAlertsQuery struct { | ||||
| 	OrgId        int64 | ||||
| 	State        []string | ||||
| 	DashboardId int64 | ||||
| 	DashboardIDs []int64 | ||||
| 	PanelId      int64 | ||||
| 	Limit        int64 | ||||
| 	Query        string | ||||
| 	User         *SignedInUser | ||||
| 
 | ||||
| 	Result []*AlertListItemDTO | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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: '@', | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue