diff --git a/grafana b/grafana index d03949a735f..37ba2511d5a 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit d03949a735fd6ee486e278feb3b87f252be5ce96 +Subproject commit 37ba2511d5aef8034b3f275e581e0c4206823854 diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 329779e1867..9746f4e7c12 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -1,6 +1,7 @@ package api import ( + "regexp" "strings" "github.com/torkelo/grafana-pro/pkg/bus" @@ -49,8 +50,8 @@ func DeleteDashboard(c *middleware.Context) { func Search(c *middleware.Context) { queryText := c.Query("q") result := m.SearchResult{ - Dashboards: []m.DashboardSearchHit{}, - Tags: []m.DashboardTagCloudItem{}, + Dashboards: []*m.DashboardSearchHit{}, + Tags: []*m.DashboardTagCloudItem{}, } if strings.HasPrefix(queryText, "tags!:") { @@ -63,8 +64,13 @@ func Search(c *middleware.Context) { result.Tags = query.Result result.TagsOnly = true } else { - queryText := strings.TrimPrefix(queryText, "title:") - query := m.SearchDashboardsQuery{Query: queryText, AccountId: c.GetAccountId()} + searchQueryRegEx, _ := regexp.Compile(`(tags:(\w*)\sAND\s)?(?:title:)?(.*)?`) + matches := searchQueryRegEx.FindStringSubmatch(queryText) + query := m.SearchDashboardsQuery{ + Title: matches[3], + Tag: matches[2], + AccountId: c.GetAccountId(), + } err := bus.Dispatch(&query) if err != nil { c.JsonApiErr(500, "Search failed", err) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index ab7ffb50e5d..481df529277 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -18,22 +18,20 @@ type Dashboard struct { Slug string `xorm:"index(IX_AccountIdSlug)"` AccountId int64 `xorm:"index(IX_AccountIdSlug)"` - Created time.Time `xorm:"CREATED"` - Updated time.Time `xorm:"UPDATED"` + Created time.Time + Updated time.Time Title string - Tags []string Data map[string]interface{} } type SearchResult struct { - Dashboards []DashboardSearchHit `json:"dashboards"` - Tags []DashboardTagCloudItem `json:"tags"` - TagsOnly bool `json:"tagsOnly"` + Dashboards []*DashboardSearchHit `json:"dashboards"` + Tags []*DashboardTagCloudItem `json:"tags"` + TagsOnly bool `json:"tagsOnly"` } type DashboardSearchHit struct { - Id string `json:"id"` Title string `json:"title"` Slug string `json:"slug"` Tags []string `json:"tags"` @@ -45,15 +43,16 @@ type DashboardTagCloudItem struct { } type SearchDashboardsQuery struct { - Query string + Title string + Tag string AccountId int64 - Result []DashboardSearchHit + Result []*DashboardSearchHit } type GetDashboardTagsQuery struct { AccountId int64 - Result []DashboardTagCloudItem + Result []*DashboardTagCloudItem } type SaveDashboardCommand struct { @@ -75,15 +74,6 @@ type GetDashboardQuery struct { Result *Dashboard } -func convertToStringArray(arr []interface{}) []string { - b := make([]string, len(arr)) - for i := range arr { - b[i] = arr[i].(string) - } - - return b -} - func NewDashboard(title string) *Dashboard { dash := &Dashboard{} dash.Data = make(map[string]interface{}) @@ -93,12 +83,25 @@ func NewDashboard(title string) *Dashboard { return dash } +func (dash *Dashboard) GetTags() []string { + jsonTags := dash.Data["tags"] + if jsonTags == nil { + return []string{} + } + + arr := jsonTags.([]interface{}) + b := make([]string, len(arr)) + for i := range arr { + b[i] = arr[i].(string) + } + return b +} + func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := &Dashboard{} dash.Data = cmd.Dashboard dash.Title = dash.Data["title"].(string) dash.AccountId = cmd.AccountId - dash.Tags = convertToStringArray(dash.Data["tags"].([]interface{})) dash.UpdateSlug() if dash.Data["id"] != nil { diff --git a/pkg/stores/sqlstore/dashboards.go b/pkg/stores/sqlstore/dashboards.go index f3c64c56f01..62a2e6c4390 100644 --- a/pkg/stores/sqlstore/dashboards.go +++ b/pkg/stores/sqlstore/dashboards.go @@ -35,6 +35,22 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { _, err = sess.Id(dash.Id).Update(dash) } + // delete existing tabs + _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) + if err != nil { + return err + } + + // insert new tags + tags := dash.GetTags() + if len(tags) > 0 { + tagRows := make([]DashboardTag, len(tags)) + for _, tag := range tags { + tagRows = append(tagRows, DashboardTag{Term: tag, DashboardId: dash.Id}) + } + sess.InsertMulti(&tagRows) + } + cmd.Result = dash return err @@ -55,24 +71,59 @@ func GetDashboard(query *m.GetDashboardQuery) error { return nil } +type DashboardSearchProjection struct { + Id int64 + Title string + Slug string + Term string +} + func SearchDashboards(query *m.SearchDashboardsQuery) error { - titleQuery := "%" + query.Query + "%" + titleQuery := "%" + query.Title + "%" - sess := x.Limit(100, 0).Where("account_id=? AND title LIKE ?", query.AccountId, titleQuery) - sess.Table("Dashboard") + sess := x.Table("dashboard") + sess.Join("LEFT OUTER", "dashboard_tag", "dashboard.id=dashboard_tag.dashboard_id") + sess.Where("account_id=? AND title LIKE ?", query.AccountId, titleQuery) + sess.Cols("dashboard.id", "dashboard.title", "dashboard.slug", "dashboard_tag.term") + sess.Limit(100, 0) - query.Result = make([]m.DashboardSearchHit, 0) - err := sess.Find(&query.Result) + if len(query.Tag) > 0 { + sess.And("dashboard_tag.term=?", query.Tag) + } + + var res []DashboardSearchProjection + err := sess.Find(&res) + if err != nil { + return err + } + + query.Result = make([]*m.DashboardSearchHit, 0) + hits := make(map[int64]*m.DashboardSearchHit) + + for _, item := range res { + hit, exists := hits[item.Id] + if !exists { + hit = &m.DashboardSearchHit{ + Title: item.Title, + Slug: item.Slug, + Tags: []string{}, + } + query.Result = append(query.Result, hit) + hits[item.Id] = hit + } + if len(item.Term) > 0 { + hit.Tags = append(hit.Tags, item.Term) + } + } return err } func GetDashboardTags(query *m.GetDashboardTagsQuery) error { - query.Result = []m.DashboardTagCloudItem{ - m.DashboardTagCloudItem{Term: "test", Count: 10}, - m.DashboardTagCloudItem{Term: "prod", Count: 20}, - } - return nil + sess := x.Sql("select count() as count, term from dashboard_tag group by term") + + err := sess.Find(&query.Result) + return err } func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { diff --git a/pkg/stores/sqlstore/dashboards_test.go b/pkg/stores/sqlstore/dashboards_test.go index 3aebd796a81..7e9aba2a47b 100644 --- a/pkg/stores/sqlstore/dashboards_test.go +++ b/pkg/stores/sqlstore/dashboards_test.go @@ -51,7 +51,7 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Should be able to search for dashboard", func() { query := m.SearchDashboardsQuery{ - Query: "%test%", + Title: "%test%", AccountId: 1, } @@ -59,6 +59,20 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) + hit := query.Result[0] + So(len(hit.Tags), ShouldEqual, 2) + }) + + Convey("Should be able to search for dashboards using tags", func() { + query1 := m.SearchDashboardsQuery{Tag: "webapp", AccountId: 1} + query2 := m.SearchDashboardsQuery{Tag: "tagdoesnotexist", AccountId: 1} + + err := SearchDashboards(&query1) + err = SearchDashboards(&query2) + So(err, ShouldBeNil) + + So(len(query1.Result), ShouldEqual, 1) + So(len(query2.Result), ShouldEqual, 0) }) Convey("Should not be able to save dashboard with same name", func() { @@ -67,7 +81,7 @@ func TestDashboardDataAccess(t *testing.T) { Dashboard: map[string]interface{}{ "id": nil, "title": "test dash 23", - "tags": make([]interface{}, 0), + "tags": []interface{}{}, }, } @@ -75,8 +89,14 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("Should be able to get dashboard tags", func() { + query := m.GetDashboardTagsQuery{} + + err := GetDashboardTags(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 3) + }) }) - }) - } diff --git a/pkg/stores/sqlstore/sqlstore.go b/pkg/stores/sqlstore/sqlstore.go index ad992f8d71d..1dc27a79804 100644 --- a/pkg/stores/sqlstore/sqlstore.go +++ b/pkg/stores/sqlstore/sqlstore.go @@ -27,11 +27,17 @@ var ( UseSQLite3 bool ) +type DashboardTag struct { + Id int64 + DashboardId int64 + Term string +} + func init() { tables = make([]interface{}, 0) tables = append(tables, new(m.Account), new(m.Dashboard), - new(m.Collaborator), new(m.DataSource)) + new(m.Collaborator), new(m.DataSource), new(DashboardTag)) } func Init() {