Major refactorings around searching, moved to seperate package, trying to move stuff out of models package, extend search support searching different types of entities and different types of dashboards, #960

This commit is contained in:
Torkel Ödegaard 2015-05-13 13:36:13 +02:00
parent c8146e759f
commit 448a8b8d1c
19 changed files with 143 additions and 128 deletions

View File

@ -216,7 +216,6 @@ exchange = grafana_events
#################################### Dashboard JSON files ########################## #################################### Dashboard JSON files ##########################
[dashboards.json] [dashboards.json]
enabled = false enabled = false
path = dashboards path = /var/lib/grafana/dashboards
orgs = *

View File

@ -211,3 +211,11 @@
;enabled = false ;enabled = false
;rabbitmq_url = amqp://localhost/ ;rabbitmq_url = amqp://localhost/
;exchange = grafana_events ;exchange = grafana_events
;#################################### Dashboard JSON files ##########################
[dashboards.json]
;enabled = false
;path = /var/lib/grafana/dashboards

View File

@ -14,8 +14,8 @@ import (
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/search"
"github.com/grafana/grafana/pkg/services/eventpublisher" "github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social" "github.com/grafana/grafana/pkg/social"

View File

@ -10,7 +10,7 @@ import (
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/search"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )

View File

@ -3,7 +3,7 @@ package api
import ( import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/search"
) )
func Search(c *middleware.Context) { func Search(c *middleware.Context) {

View File

@ -126,3 +126,13 @@ type GetDashboardQuery struct {
Result *Dashboard Result *Dashboard
} }
type DashboardTagCloudItem struct {
Term string `json:"term"`
Count int `json:"count"`
}
type GetDashboardTagsQuery struct {
OrgId int64
Result []*DashboardTagCloudItem
}

View File

@ -1,12 +1,6 @@
package models package models
type SearchResult struct { type SearchHit struct {
Dashboards []*DashboardSearchHit `json:"dashboards"`
Tags []*DashboardTagCloudItem `json:"tags"`
TagsOnly bool `json:"tagsOnly"`
}
type DashboardSearchHit struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Uri string `json:"uri"` Uri string `json:"uri"`
@ -14,24 +8,3 @@ type DashboardSearchHit struct {
Tags []string `json:"tags"` Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"` IsStarred bool `json:"isStarred"`
} }
type DashboardTagCloudItem struct {
Term string `json:"term"`
Count int `json:"count"`
}
type SearchDashboardsQuery struct {
Title string
Tag string
OrgId int64
UserId int64
Limit int
IsStarred bool
Result []*DashboardSearchHit
}
type GetDashboardTagsQuery struct {
OrgId int64
Result []*DashboardTagCloudItem
}

View File

@ -2,23 +2,13 @@ package search
import ( import (
"path/filepath" "path/filepath"
"sort"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
type Query struct {
Title string
Tag string
OrgId int64
UserId int64
Limit int
IsStarred bool
Result []*m.DashboardSearchHit
}
var jsonDashIndex *JsonDashIndex var jsonDashIndex *JsonDashIndex
func Init() { func Init() {
@ -33,15 +23,14 @@ func Init() {
jsonFilesPath = filepath.Join(setting.HomePath, jsonFilesPath) jsonFilesPath = filepath.Join(setting.HomePath, jsonFilesPath)
} }
orgIds := jsonIndexCfg.Key("org_ids").String() jsonDashIndex = NewJsonDashIndex(jsonFilesPath)
jsonDashIndex = NewJsonDashIndex(jsonFilesPath, orgIds)
} }
} }
func searchHandler(query *Query) error { func searchHandler(query *Query) error {
hits := make([]*m.DashboardSearchHit, 0) hits := make(HitList, 0)
dashQuery := m.SearchDashboardsQuery{ dashQuery := FindPersistedDashboardsQuery{
Title: query.Title, Title: query.Title,
Tag: query.Tag, Tag: query.Tag,
UserId: query.UserId, UserId: query.UserId,
@ -65,6 +54,8 @@ func searchHandler(query *Query) error {
hits = append(hits, jsonHits...) hits = append(hits, jsonHits...)
} }
sort.Sort(hits)
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
return err return err
} }
@ -73,7 +64,7 @@ func searchHandler(query *Query) error {
return nil return nil
} }
func setIsStarredFlagOnSearchResults(userId int64, hits []*m.DashboardSearchHit) error { func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
query := m.GetUserStarsQuery{UserId: userId} query := m.GetUserStarsQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
return err return err

View File

@ -11,9 +11,8 @@ import (
) )
type JsonDashIndex struct { type JsonDashIndex struct {
path string path string
orgsIds []int64 items []*JsonDashIndexItem
items []*JsonDashIndexItem
} }
type JsonDashIndexItem struct { type JsonDashIndexItem struct {
@ -23,7 +22,7 @@ type JsonDashIndexItem struct {
Dashboard *m.Dashboard Dashboard *m.Dashboard
} }
func NewJsonDashIndex(path string, orgIds string) *JsonDashIndex { func NewJsonDashIndex(path string) *JsonDashIndex {
log.Info("Creating json dashboard index for path: ", path) log.Info("Creating json dashboard index for path: ", path)
index := JsonDashIndex{} index := JsonDashIndex{}
@ -32,8 +31,8 @@ func NewJsonDashIndex(path string, orgIds string) *JsonDashIndex {
return &index return &index
} }
func (index *JsonDashIndex) Search(query *Query) ([]*m.DashboardSearchHit, error) { func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) {
results := make([]*m.DashboardSearchHit, 0) results := make([]*Hit, 0)
for _, item := range index.items { for _, item := range index.items {
if len(results) > query.Limit { if len(results) > query.Limit {
@ -49,8 +48,8 @@ func (index *JsonDashIndex) Search(query *Query) ([]*m.DashboardSearchHit, error
// add results with matchig title filter // add results with matchig title filter
if strings.Contains(item.TitleLower, query.Title) { if strings.Contains(item.TitleLower, query.Title) {
results = append(results, &m.DashboardSearchHit{ results = append(results, &Hit{
Type: m.DashTypeJson, Type: DashHitJson,
Title: item.Dashboard.Title, Title: item.Dashboard.Title,
Tags: item.Dashboard.GetTags(), Tags: item.Dashboard.GetTags(),
Uri: "file/" + item.Path, Uri: "file/" + item.Path,

View File

@ -9,7 +9,7 @@ import (
func TestJsonDashIndex(t *testing.T) { func TestJsonDashIndex(t *testing.T) {
Convey("Given the json dash index", t, func() { Convey("Given the json dash index", t, func() {
index := NewJsonDashIndex("../../../public/dashboards/", "*") index := NewJsonDashIndex("../../public/dashboards/", "*")
Convey("Should be able to update index", func() { Convey("Should be able to update index", func() {
err := index.updateIndex() err := index.updateIndex()

47
pkg/search/models.go Normal file
View File

@ -0,0 +1,47 @@
package search
type HitType string
const (
DashHitDB HitType = "dash-db"
DashHitHome HitType = "dash-home"
DashHitJson HitType = "dash-json"
DashHitScripted HitType = "dash-scripted"
)
type Hit struct {
Id int64 `json:"id"`
Title string `json:"title"`
Uri string `json:"uri"`
Type HitType `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`
}
type HitList []*Hit
func (s HitList) Len() int { return len(s) }
func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title }
type Query struct {
Title string
Tag string
OrgId int64
UserId int64
Limit int
IsStarred bool
Result HitList
}
type FindPersistedDashboardsQuery struct {
Title string
Tag string
OrgId int64
UserId int64
Limit int
IsStarred bool
Result HitList
}

View File

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/search"
) )
func init() { func init() {
@ -119,7 +120,7 @@ type DashboardSearchProjection struct {
Term string Term string
} }
func SearchDashboards(query *m.SearchDashboardsQuery) error { func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
var sql bytes.Buffer var sql bytes.Buffer
params := make([]interface{}, 0) params := make([]interface{}, 0)
@ -166,17 +167,17 @@ func SearchDashboards(query *m.SearchDashboardsQuery) error {
return err return err
} }
query.Result = make([]*m.DashboardSearchHit, 0) query.Result = make([]*search.Hit, 0)
hits := make(map[int64]*m.DashboardSearchHit) hits := make(map[int64]*search.Hit)
for _, item := range res { for _, item := range res {
hit, exists := hits[item.Id] hit, exists := hits[item.Id]
if !exists { if !exists {
hit = &m.DashboardSearchHit{ hit = &search.Hit{
Id: item.Id, Id: item.Id,
Title: item.Title, Title: item.Title,
Uri: "db/" + item.Slug, Uri: "db/" + item.Slug,
Type: m.DashTypeDB, Type: search.DashHitDB,
Tags: []string{}, Tags: []string{},
} }
query.Result = append(query.Result, hit) query.Result = append(query.Result, hit)

View File

@ -6,6 +6,7 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/search"
) )
func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard { func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard {
@ -85,7 +86,7 @@ func TestDashboardDataAccess(t *testing.T) {
}) })
Convey("Should be able to search for dashboard", func() { Convey("Should be able to search for dashboard", func() {
query := m.SearchDashboardsQuery{ query := search.FindPersistedDashboardsQuery{
Title: "test", Title: "test",
OrgId: 1, OrgId: 1,
} }
@ -99,8 +100,8 @@ func TestDashboardDataAccess(t *testing.T) {
}) })
Convey("Should be able to search for dashboards using tags", func() { Convey("Should be able to search for dashboards using tags", func() {
query1 := m.SearchDashboardsQuery{Tag: "webapp", OrgId: 1} query1 := search.FindPersistedDashboardsQuery{Tag: "webapp", OrgId: 1}
query2 := m.SearchDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1} query2 := search.FindPersistedDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1}
err := SearchDashboards(&query1) err := SearchDashboards(&query1)
err = SearchDashboards(&query2) err = SearchDashboards(&query2)
@ -146,7 +147,7 @@ func TestDashboardDataAccess(t *testing.T) {
}) })
Convey("Should be able to search for starred dashboards", func() { Convey("Should be able to search for starred dashboards", func() {
query := m.SearchDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true} query := search.FindPersistedDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true}
err := SearchDashboards(&query) err := SearchDashboards(&query)
So(err, ShouldBeNil) So(err, ShouldBeNil)

View File

@ -40,15 +40,15 @@ function (angular, _, config) {
$scope.moveSelection(-1); $scope.moveSelection(-1);
} }
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
if ($scope.query.tagcloud) { if ($scope.tagMode) {
var tag = $scope.results.tags[$scope.selectedIndex]; var tag = $scope.results[$scope.selectedIndex];
if (tag) { if (tag) {
$scope.filterByTag(tag.term); $scope.filterByTag(tag.term);
} }
return; return;
} }
var selectedDash = $scope.results.dashboards[$scope.selectedIndex]; var selectedDash = $scope.results[$scope.selectedIndex];
if (selectedDash) { if (selectedDash) {
$location.search({}); $location.search({});
$location.path(selectedDash.url); $location.path(selectedDash.url);
@ -57,7 +57,9 @@ function (angular, _, config) {
}; };
$scope.moveSelection = function(direction) { $scope.moveSelection = function(direction) {
$scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0); var max = ($scope.results || []).length;
var newIndex = $scope.selectedIndex + direction;
$scope.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex;
}; };
$scope.searchDashboards = function() { $scope.searchDashboards = function() {
@ -68,14 +70,13 @@ function (angular, _, config) {
return backendSrv.search($scope.query).then(function(results) { return backendSrv.search($scope.query).then(function(results) {
if (localSearchId < $scope.currentSearchId) { return; } if (localSearchId < $scope.currentSearchId) { return; }
$scope.resultCount = results.length;
$scope.results = _.map(results, function(dash) { $scope.results = _.map(results, function(dash) {
dash.url = 'dashboard/' + dash.uri; dash.url = 'dashboard/' + dash.uri;
return dash; return dash;
}); });
if ($scope.queryHasNoFilters()) { if ($scope.queryHasNoFilters()) {
$scope.results.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); $scope.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' });
} }
}); });
}; };
@ -97,10 +98,10 @@ function (angular, _, config) {
}; };
$scope.getTags = function() { $scope.getTags = function() {
$scope.tagsMode = true;
return backendSrv.get('/api/dashboards/tags').then(function(results) { return backendSrv.get('/api/dashboards/tags').then(function(results) {
$scope.resultCount = results.length; $scope.tagsMode = true;
$scope.results = results; $scope.results = results;
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
}); });
}; };
@ -116,26 +117,6 @@ function (angular, _, config) {
$scope.searchDashboards(); $scope.searchDashboards();
}; };
$scope.addMetricToCurrentDashboard = function (metricId) {
$scope.dashboard.rows.push({
title: '',
height: '250px',
editable: true,
panels: [
{
type: 'graphite',
title: 'test',
span: 12,
targets: [{ target: metricId }]
}
]
});
};
$scope.toggleImport = function () {
$scope.showImport = !$scope.showImport;
};
$scope.newDashboard = function() { $scope.newDashboard = function() {
$location.url('dashboard/new'); $location.url('dashboard/new');
}; };

View File

@ -133,12 +133,12 @@ function (angular, _) {
$scope.searchDashboards = function(link) { $scope.searchDashboards = function(link) {
return backendSrv.search({tag: link.tag}).then(function(results) { return backendSrv.search({tag: link.tag}).then(function(results) {
return _.reduce(results.dashboards, function(memo, dash) { return _.reduce(results, function(memo, dash) {
// do not add current dashboard // do not add current dashboard
if (dash.id !== currentDashId) { if (dash.id !== currentDashId) {
memo.push({ memo.push({
title: dash.title, title: dash.title,
url: 'dashboard/db/'+ dash.slug, url: 'dashboard/' + dash.uri,
icon: 'fa fa-th-large', icon: 'fa fa-th-large',
keepTime: link.keepTime, keepTime: link.keepTime,
includeVars: link.includeVars includeVars: link.includeVars

View File

@ -1,7 +1,7 @@
<grafana-panel> <grafana-panel>
<div class="dashlist"> <div class="dashlist">
<div class="dashlist-item" ng-repeat="dash in dashList"> <div class="dashlist-item" ng-repeat="dash in dashList">
<a class="dashlist-link" href="dashboard/{{dash.uri}}"> <a class="dashlist-link dashlist-link-{{dash.type}}" href="dashboard/{{dash.uri}}">
<span class="dashlist-title"> <span class="dashlist-title">
{{dash.title}} {{dash.title}}
</span> </span>

View File

@ -62,7 +62,7 @@ function (angular, app, _, config, PanelMeta) {
} }
return backendSrv.search(params).then(function(result) { return backendSrv.search(params).then(function(result) {
$scope.dashList = result.dashboards; $scope.dashList = result;
}); });
}; };

View File

@ -24,41 +24,39 @@
</div> </div>
</div> </div>
<div ng-if="!showImport"> <div class="search-results-container" ng-if="tagsMode">
<div class="search-results-container" ng-if="tagsMode"> <div class="row">
<div class="row"> <div class="span6 offset1">
<div class="span6 offset1"> <div ng-repeat="tag in results" class="pointer" style="width: 180px; float: left;"
<div ng-repeat="tag in results" class="pointer" style="width: 180px; float: left;" ng-class="{'selected': $index === selectedIndex }"
ng-class="{'selected': $index === selectedIndex }" ng-click="filterByTag(tag.term, $event)">
ng-click="filterByTag(tag.term, $event)"> <a class="search-result-tag label label-tag" tag-color-from-name tag="tag.term">
<a class="search-result-tag label label-tag" tag-color-from-name tag="tag.term"> <i class="fa fa-tag"></i>
<i class="fa fa-tag"></i> <span>{{tag.term}} &nbsp;({{tag.count}})</span>
<span>{{tag.term}} &nbsp;({{tag.count}})</span> </a>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="search-results-container" ng-if="!tagsMode"> <div class="search-results-container" ng-if="!tagsMode">
<h6 ng-hide="results.length">No dashboards matching your query were found.</h6> <h6 ng-hide="results.length">No dashboards matching your query were found.</h6>
<a class="search-result-item pointer search-result-item-{{row.type}}" bindonce ng-repeat="row in results" <a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in results"
ng-class="{'selected': $index == selectedIndex}" ng-href="{{row.url}}"> ng-class="{'selected': $index == selectedIndex}" ng-href="{{row.url}}">
<span class="search-result-tags"> <span class="search-result-tags">
<span ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name tag="tag" class="label label-tag"> <span ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name tag="tag" class="label label-tag">
{{tag}} {{tag}}
</span>
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
</span> </span>
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
</span>
<span class="search-result-link"> <span class="search-result-link">
<i class="search-result-icon"></i> <i class="fa search-result-icon"></i>
<span bo-text="row.title"></span> <span bo-text="row.title"></span>
</span> </span>
</a> </a>
</div>
</div> </div>
<div class="search-button-row"> <div class="search-button-row">

View File

@ -41,7 +41,7 @@
display: block; display: block;
line-height: 28px; line-height: 28px;
.search-result-item:hover, .search-result-item.selected { .search-item:hover, .search-item.selected {
background-color: @grafanaListHighlight; background-color: @grafanaListHighlight;
} }
@ -67,12 +67,19 @@
} }
} }
.search-result-item { .search-item {
display: block; display: block;
padding: 3px 10px; padding: 3px 10px;
white-space: nowrap; white-space: nowrap;
background-color: @grafanaListBackground; background-color: @grafanaListBackground;
margin-bottom: 4px; margin-bottom: 4px;
.search-result-icon:before {
content: "\f009";
}
&.search-item-dash-home .search-result-icon:before {
content: "\f015";
}
} }
.search-result-tags { .search-result-tags {