mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SearchV2: instant local updates, folder events (#50001)
This commit is contained in:
parent
49d93fb67e
commit
d2868a1ce7
@ -13,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/store"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@ -70,6 +71,14 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
if hs.entityEventsService != nil {
|
||||
if err := hs.entityEventsService.SaveEvent(c.Req.Context(), store.SaveEventCmd{
|
||||
EntityId: store.CreateDatabaseEntityId(folder.Uid, c.OrgId, store.EntityTypeFolder),
|
||||
EventType: store.EntityEventTypeCreate,
|
||||
}); err != nil {
|
||||
hs.log.Warn("failed to save folder entity event", "uid", folder.Uid, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), folder.Id, c.OrgId, c.SignedInUser)
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, folder))
|
||||
@ -84,6 +93,14 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext) response.Response {
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
if hs.entityEventsService != nil {
|
||||
if err := hs.entityEventsService.SaveEvent(c.Req.Context(), store.SaveEventCmd{
|
||||
EntityId: store.CreateDatabaseEntityId(cmd.Uid, c.OrgId, store.EntityTypeFolder),
|
||||
EventType: store.EntityEventTypeUpdate,
|
||||
}); err != nil {
|
||||
hs.log.Warn("failed to save folder entity event", "uid", cmd.Uid, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||
return response.JSON(http.StatusOK, hs.toFolderDto(c.Req.Context(), g, cmd.Result))
|
||||
@ -98,10 +115,19 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { //
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
f, err := hs.folderService.DeleteFolder(c.Req.Context(), c.SignedInUser, c.OrgId, web.Params(c.Req)[":uid"], c.QueryBool("forceDeleteRules"))
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
f, err := hs.folderService.DeleteFolder(c.Req.Context(), c.SignedInUser, c.OrgId, uid, c.QueryBool("forceDeleteRules"))
|
||||
if err != nil {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
if hs.entityEventsService != nil {
|
||||
if err := hs.entityEventsService.SaveEvent(c.Req.Context(), store.SaveEventCmd{
|
||||
EntityId: store.CreateDatabaseEntityId(uid, c.OrgId, store.EntityTypeFolder),
|
||||
EventType: store.EntityEventTypeDelete,
|
||||
}); err != nil {
|
||||
hs.log.Warn("failed to save folder entity event", "uid", uid, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, util.DynMap{
|
||||
"title": f.Title,
|
||||
|
@ -105,7 +105,10 @@ func initIndex(dashboards []dashboard, logger log.Logger, extendDoc ExtendDashbo
|
||||
}
|
||||
|
||||
// Index each panel in dashboard.
|
||||
location += "/" + dash.uid
|
||||
if location != "" {
|
||||
location += "/"
|
||||
}
|
||||
location += dash.uid
|
||||
docs := getDashboardPanelDocs(dash, location)
|
||||
for _, panelDoc := range docs {
|
||||
batch.Insert(panelDoc)
|
||||
@ -194,7 +197,6 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document {
|
||||
purl := fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
|
||||
|
||||
doc := newSearchDocument(uid, panel.Title, panel.Description, purl).
|
||||
AddField(bluge.NewKeywordField(documentFieldDSUID, dash.uid).StoreValue()).
|
||||
AddField(bluge.NewKeywordField(documentFieldPanelType, panel.Type).Aggregatable().StoreValue()).
|
||||
AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()).
|
||||
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindPanel)).Aggregatable().StoreValue()) // likely want independent index for this
|
||||
@ -263,10 +265,10 @@ func newSearchDocument(uid string, name string, descr string, url string) *bluge
|
||||
return doc
|
||||
}
|
||||
|
||||
func getDashboardPanelIDs(reader *bluge.Reader, dashboardUID string) ([]string, error) {
|
||||
func getDashboardPanelIDs(reader *bluge.Reader, panelLocation string) ([]string, error) {
|
||||
var panelIDs []string
|
||||
fullQuery := bluge.NewBooleanQuery()
|
||||
fullQuery.AddMust(bluge.NewTermQuery(dashboardUID).SetField(documentFieldDSUID))
|
||||
fullQuery.AddMust(bluge.NewTermQuery(panelLocation).SetField(documentFieldLocation))
|
||||
fullQuery.AddMust(bluge.NewTermQuery(string(entityKindPanel)).SetField(documentFieldKind))
|
||||
req := bluge.NewAllMatches(fullQuery)
|
||||
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||
@ -291,6 +293,64 @@ func getDashboardPanelIDs(reader *bluge.Reader, dashboardUID string) ([]string,
|
||||
return panelIDs, err
|
||||
}
|
||||
|
||||
func getDocsIDsByLocationPrefix(reader *bluge.Reader, prefix string) ([]string, error) {
|
||||
var ids []string
|
||||
fullQuery := bluge.NewBooleanQuery()
|
||||
fullQuery.AddMust(bluge.NewPrefixQuery(prefix).SetField(documentFieldLocation))
|
||||
req := bluge.NewAllMatches(fullQuery)
|
||||
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
match, err := documentMatchIterator.Next()
|
||||
for err == nil && match != nil {
|
||||
// load the identifier for this match
|
||||
err = match.VisitStoredFields(func(field string, value []byte) bool {
|
||||
if field == documentFieldUID {
|
||||
ids = append(ids, string(value))
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// load the next document match
|
||||
match, err = documentMatchIterator.Next()
|
||||
}
|
||||
return ids, err
|
||||
}
|
||||
|
||||
func getDashboardLocation(reader *bluge.Reader, dashboardUID string) (string, bool, error) {
|
||||
var dashboardLocation string
|
||||
var found bool
|
||||
fullQuery := bluge.NewBooleanQuery()
|
||||
fullQuery.AddMust(bluge.NewTermQuery(dashboardUID).SetField(documentFieldUID))
|
||||
fullQuery.AddMust(bluge.NewTermQuery(string(entityKindDashboard)).SetField(documentFieldKind))
|
||||
req := bluge.NewAllMatches(fullQuery)
|
||||
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
match, err := documentMatchIterator.Next()
|
||||
for err == nil && match != nil {
|
||||
// load the identifier for this match
|
||||
err = match.VisitStoredFields(func(field string, value []byte) bool {
|
||||
if field == documentFieldLocation {
|
||||
dashboardLocation = string(value)
|
||||
found = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
// load the next document match
|
||||
match, err = documentMatchIterator.Next()
|
||||
}
|
||||
return dashboardLocation, found, err
|
||||
}
|
||||
|
||||
//nolint: gocyclo
|
||||
func doSearchQuery(
|
||||
ctx context.Context,
|
||||
|
@ -30,6 +30,7 @@ type dashboardLoader interface {
|
||||
}
|
||||
|
||||
type eventStore interface {
|
||||
OnEvent(handler store.EventHandler)
|
||||
GetLastEvent(ctx context.Context) (*store.EntityEvent, error)
|
||||
GetAllEventsAfter(ctx context.Context, id int64) ([]*store.EntityEvent, error)
|
||||
}
|
||||
@ -95,6 +96,8 @@ func (i *dashboardIndex) run(ctx context.Context) error {
|
||||
return fmt.Errorf("can't build initial dashboard search index: %w", err)
|
||||
}
|
||||
|
||||
i.eventStore.OnEvent(i.applyEventOnIndex)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-partialUpdateTicker.C:
|
||||
@ -293,7 +296,6 @@ func (i *dashboardIndex) applyIndexUpdates(ctx context.Context, lastEventID int6
|
||||
}
|
||||
started := time.Now()
|
||||
for _, e := range events {
|
||||
i.logger.Debug("processing event", "event", e)
|
||||
err := i.applyEventOnIndex(ctx, e)
|
||||
if err != nil {
|
||||
i.logger.Error("can't apply event", "error", err)
|
||||
@ -306,31 +308,30 @@ func (i *dashboardIndex) applyIndexUpdates(ctx context.Context, lastEventID int6
|
||||
}
|
||||
|
||||
func (i *dashboardIndex) applyEventOnIndex(ctx context.Context, e *store.EntityEvent) error {
|
||||
i.logger.Debug("processing event", "event", e)
|
||||
|
||||
if !strings.HasPrefix(e.EntityId, "database/") {
|
||||
i.logger.Warn("unknown storage", "entityId", e.EntityId)
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(strings.TrimPrefix(e.EntityId, "database/"), "/")
|
||||
// database/org/entityType/path*
|
||||
parts := strings.SplitN(strings.TrimPrefix(e.EntityId, "database/"), "/", 3)
|
||||
if len(parts) != 3 {
|
||||
i.logger.Error("can't parse entityId", "entityId", e.EntityId)
|
||||
return nil
|
||||
}
|
||||
orgIDStr := parts[0]
|
||||
kind := parts[1]
|
||||
dashboardUID := parts[2]
|
||||
if kind != "dashboard" {
|
||||
i.logger.Error("unknown kind in entityId", "entityId", e.EntityId)
|
||||
return nil
|
||||
}
|
||||
orgID, err := strconv.Atoi(orgIDStr)
|
||||
orgID, err := strconv.ParseInt(orgIDStr, 10, 64)
|
||||
if err != nil {
|
||||
i.logger.Error("can't extract org ID", "entityId", e.EntityId)
|
||||
return nil
|
||||
}
|
||||
return i.applyDashboardEvent(ctx, int64(orgID), dashboardUID, e.EventType)
|
||||
kind := store.EntityType(parts[1])
|
||||
uid := parts[2]
|
||||
return i.applyEvent(ctx, orgID, kind, uid, e.EventType)
|
||||
}
|
||||
|
||||
func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, dashboardUID string, _ store.EntityEventType) error {
|
||||
func (i *dashboardIndex) applyEvent(ctx context.Context, orgID int64, kind store.EntityType, uid string, _ store.EntityEventType) error {
|
||||
i.mu.Lock()
|
||||
_, ok := i.perOrgWriter[orgID]
|
||||
if !ok {
|
||||
@ -340,7 +341,8 @@ func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, d
|
||||
}
|
||||
i.mu.Unlock()
|
||||
|
||||
dbDashboards, err := i.loader.LoadDashboards(ctx, orgID, dashboardUID)
|
||||
// Both dashboard and folder share same DB table.
|
||||
dbDashboards, err := i.loader.LoadDashboards(ctx, orgID, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -364,7 +366,14 @@ func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, d
|
||||
|
||||
// In the future we can rely on operation types to reduce work here.
|
||||
if len(dbDashboards) == 0 {
|
||||
newReader, err = i.removeDashboard(ctx, writer, reader, dashboardUID)
|
||||
switch kind {
|
||||
case store.EntityTypeDashboard:
|
||||
newReader, err = i.removeDashboard(ctx, writer, reader, uid)
|
||||
case store.EntityTypeFolder:
|
||||
newReader, err = i.removeFolder(ctx, writer, reader, uid)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
newReader, err = i.updateDashboard(ctx, orgID, writer, reader, dbDashboards[0])
|
||||
}
|
||||
@ -377,8 +386,21 @@ func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, d
|
||||
}
|
||||
|
||||
func (i *dashboardIndex) removeDashboard(_ context.Context, writer *bluge.Writer, reader *bluge.Reader, dashboardUID string) (*bluge.Reader, error) {
|
||||
dashboardLocation, ok, err := getDashboardLocation(reader, dashboardUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
// No dashboard, nothing to remove.
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// Find all panel docs to remove with dashboard.
|
||||
panelIDs, err := getDashboardPanelIDs(reader, dashboardUID)
|
||||
panelLocation := dashboardUID
|
||||
if dashboardLocation != "" {
|
||||
panelLocation = dashboardLocation + "/" + dashboardUID
|
||||
}
|
||||
panelIDs, err := getDocsIDsByLocationPrefix(reader, panelLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -397,6 +419,23 @@ func (i *dashboardIndex) removeDashboard(_ context.Context, writer *bluge.Writer
|
||||
return writer.Reader()
|
||||
}
|
||||
|
||||
func (i *dashboardIndex) removeFolder(_ context.Context, writer *bluge.Writer, reader *bluge.Reader, folderUID string) (*bluge.Reader, error) {
|
||||
ids, err := getDocsIDsByLocationPrefix(reader, folderUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batch := bluge.NewBatch()
|
||||
batch.Delete(bluge.NewDocument(folderUID).ID())
|
||||
for _, id := range ids {
|
||||
batch.Delete(bluge.NewDocument(id).ID())
|
||||
}
|
||||
err = writer.Batch(batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writer.Reader()
|
||||
}
|
||||
|
||||
func stringInSlice(str string, slice []string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
@ -437,14 +476,17 @@ func (i *dashboardIndex) updateDashboard(ctx context.Context, orgID int64, write
|
||||
|
||||
var actualPanelIDs []string
|
||||
|
||||
location += "/" + dash.uid
|
||||
if location != "" {
|
||||
location += "/"
|
||||
}
|
||||
location += dash.uid
|
||||
panelDocs := getDashboardPanelDocs(dash, location)
|
||||
for _, panelDoc := range panelDocs {
|
||||
actualPanelIDs = append(actualPanelIDs, string(panelDoc.ID().Term()))
|
||||
batch.Update(panelDoc.ID(), panelDoc)
|
||||
}
|
||||
|
||||
indexedPanelIDs, err := getDashboardPanelIDs(reader, dash.uid)
|
||||
indexedPanelIDs, err := getDashboardPanelIDs(reader, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -2,15 +2,17 @@ package searchV2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2/extract"
|
||||
"github.com/grafana/grafana/pkg/services/store"
|
||||
|
||||
"github.com/blugelabs/bluge"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -370,3 +372,142 @@ func TestDashboardIndex_MultipleTokensScattered(t *testing.T) {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
var dashboardsWithFolders = []dashboard{
|
||||
{
|
||||
id: 1,
|
||||
uid: "1",
|
||||
isFolder: true,
|
||||
info: &extract.DashboardInfo{
|
||||
Title: "My folder",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
uid: "2",
|
||||
folderID: 1,
|
||||
info: &extract.DashboardInfo{
|
||||
Title: "Dashboard in folder 1",
|
||||
Panels: []extract.PanelInfo{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Panel 1",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Title: "Panel 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
uid: "3",
|
||||
folderID: 1,
|
||||
info: &extract.DashboardInfo{
|
||||
Title: "Dashboard in folder 2",
|
||||
Panels: []extract.PanelInfo{
|
||||
{
|
||||
ID: 3,
|
||||
Title: "Panel 3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
uid: "4",
|
||||
info: &extract.DashboardInfo{
|
||||
Title: "One more dash",
|
||||
Panels: []extract.PanelInfo{
|
||||
{
|
||||
ID: 3,
|
||||
Title: "Panel 4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestDashboardIndex_Folders(t *testing.T) {
|
||||
t.Run("folders-indexed", func(t *testing.T) {
|
||||
_, reader, _ := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||
checkSearchResponse(t, filepath.Base(t.Name()), reader, testAllowAllFilter,
|
||||
DashboardQuery{Query: "My folder", Kind: []string{string(entityKindFolder)}},
|
||||
)
|
||||
})
|
||||
t.Run("folders-dashboard-has-folder", func(t *testing.T) {
|
||||
_, reader, _ := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||
// TODO: golden file compare does not work here.
|
||||
resp := doSearchQuery(context.Background(), testLogger, reader, testAllowAllFilter,
|
||||
DashboardQuery{Query: "Dashboard in folder", Kind: []string{string(entityKindDashboard)}},
|
||||
&NoopQueryExtender{}, "")
|
||||
custom, ok := resp.Frames[0].Meta.Custom.(*customMeta)
|
||||
require.Equal(t, uint64(2), custom.Count)
|
||||
require.True(t, ok, fmt.Sprintf("actual type: %T", resp.Frames[0].Meta.Custom))
|
||||
require.Equal(t, "/dashboards/f/1/", custom.Locations["1"].URL)
|
||||
})
|
||||
t.Run("folders-dashboard-removed-on-folder-removed", func(t *testing.T) {
|
||||
index, reader, writer := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||
newReader, err := index.removeFolder(context.Background(), writer, reader, "1")
|
||||
require.NoError(t, err)
|
||||
// In response we expect one dashboard which does not belong to removed folder.
|
||||
checkSearchResponse(t, filepath.Base(t.Name()), newReader, testAllowAllFilter,
|
||||
DashboardQuery{Query: "dash", Kind: []string{string(entityKindDashboard)}},
|
||||
)
|
||||
})
|
||||
t.Run("folders-panels-removed-on-folder-removed", func(t *testing.T) {
|
||||
index, reader, writer := initTestIndexFromDashes(t, dashboardsWithFolders)
|
||||
newReader, err := index.removeFolder(context.Background(), writer, reader, "1")
|
||||
require.NoError(t, err)
|
||||
resp := doSearchQuery(context.Background(), testLogger, newReader, testAllowAllFilter,
|
||||
DashboardQuery{Query: "Panel", Kind: []string{string(entityKindPanel)}},
|
||||
&NoopQueryExtender{}, "")
|
||||
custom, ok := resp.Frames[0].Meta.Custom.(*customMeta)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, uint64(1), custom.Count) // 1 panel which does not belong to dashboards in removed folder.
|
||||
})
|
||||
}
|
||||
|
||||
var dashboardsWithPanels = []dashboard{
|
||||
{
|
||||
id: 1,
|
||||
uid: "1",
|
||||
info: &extract.DashboardInfo{
|
||||
Title: "My Dash",
|
||||
Panels: []extract.PanelInfo{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Panel 1",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Title: "Panel 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestDashboardIndex_Panels(t *testing.T) {
|
||||
t.Run("panels-indexed", func(t *testing.T) {
|
||||
_, reader, _ := initTestIndexFromDashes(t, dashboardsWithPanels)
|
||||
// TODO: golden file compare does not work here.
|
||||
resp := doSearchQuery(
|
||||
context.Background(), testLogger, reader, testAllowAllFilter,
|
||||
DashboardQuery{Query: "Panel", Kind: []string{string(entityKindPanel)}},
|
||||
&NoopQueryExtender{}, "")
|
||||
custom, ok := resp.Frames[0].Meta.Custom.(*customMeta)
|
||||
require.True(t, ok, fmt.Sprintf("actual type: %T", resp.Frames[0].Meta.Custom))
|
||||
require.Equal(t, uint64(2), custom.Count)
|
||||
require.Equal(t, "/d/1/", custom.Locations["1"].URL)
|
||||
})
|
||||
t.Run("panels-panel-removed-on-dashboard-removed", func(t *testing.T) {
|
||||
index, reader, writer := initTestIndexFromDashes(t, dashboardsWithPanels)
|
||||
newReader, err := index.removeDashboard(context.Background(), writer, reader, "1")
|
||||
require.NoError(t, err)
|
||||
checkSearchResponse(t, filepath.Base(t.Name()), newReader, testAllowAllFilter,
|
||||
DashboardQuery{Query: "Panel", Kind: []string{string(entityKindPanel)}},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
131
pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc
vendored
Normal file
131
pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
// 🌟 This was machine generated. Do not edit. 🌟
|
||||
//
|
||||
// Frame[0] {
|
||||
// "type": "search-results",
|
||||
// "custom": {
|
||||
// "count": 1
|
||||
// }
|
||||
// }
|
||||
// Name: Query results
|
||||
// Dimensions: 8 Fields by 1 Rows
|
||||
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||
// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | null | |
|
||||
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||
//
|
||||
//
|
||||
// 🌟 This was machine generated. Do not edit. 🌟
|
||||
{
|
||||
"frames": [
|
||||
{
|
||||
"schema": {
|
||||
"name": "Query results",
|
||||
"meta": {
|
||||
"type": "search-results",
|
||||
"custom": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "kind",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "uid",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "panel_type",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
},
|
||||
"config": {
|
||||
"links": [
|
||||
{
|
||||
"title": "link",
|
||||
"url": "${__value.text}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ds_uid",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
[
|
||||
"dashboard"
|
||||
],
|
||||
[
|
||||
"4"
|
||||
],
|
||||
[
|
||||
"One more dash"
|
||||
],
|
||||
[
|
||||
""
|
||||
],
|
||||
[
|
||||
"/pfix/d/4/"
|
||||
],
|
||||
[
|
||||
null
|
||||
],
|
||||
[
|
||||
null
|
||||
],
|
||||
[
|
||||
""
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
131
pkg/services/searchV2/testdata/folders-indexed.jsonc
vendored
Normal file
131
pkg/services/searchV2/testdata/folders-indexed.jsonc
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
// 🌟 This was machine generated. Do not edit. 🌟
|
||||
//
|
||||
// Frame[0] {
|
||||
// "type": "search-results",
|
||||
// "custom": {
|
||||
// "count": 1
|
||||
// }
|
||||
// }
|
||||
// Name: Query results
|
||||
// Dimensions: 8 Fields by 1 Rows
|
||||
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
|
||||
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
|
||||
// | folder | 1 | My folder | | /pfix/dashboards/f/1/ | null | null | |
|
||||
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
|
||||
//
|
||||
//
|
||||
// 🌟 This was machine generated. Do not edit. 🌟
|
||||
{
|
||||
"frames": [
|
||||
{
|
||||
"schema": {
|
||||
"name": "Query results",
|
||||
"meta": {
|
||||
"type": "search-results",
|
||||
"custom": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "kind",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "uid",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "panel_type",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
},
|
||||
"config": {
|
||||
"links": [
|
||||
{
|
||||
"title": "link",
|
||||
"url": "${__value.text}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ds_uid",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
[
|
||||
"folder"
|
||||
],
|
||||
[
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"My folder"
|
||||
],
|
||||
[
|
||||
""
|
||||
],
|
||||
[
|
||||
"/pfix/dashboards/f/1/"
|
||||
],
|
||||
[
|
||||
null
|
||||
],
|
||||
[
|
||||
null
|
||||
],
|
||||
[
|
||||
""
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
114
pkg/services/searchV2/testdata/panels-panel-removed-on-dashboard-removed.jsonc
vendored
Normal file
114
pkg/services/searchV2/testdata/panels-panel-removed-on-dashboard-removed.jsonc
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
// 🌟 This was machine generated. Do not edit. 🌟
|
||||
//
|
||||
// Frame[0] {
|
||||
// "type": "search-results",
|
||||
// "custom": {
|
||||
// "count": 0
|
||||
// }
|
||||
// }
|
||||
// Name: Query results
|
||||
// Dimensions: 8 Fields by 0 Rows
|
||||
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||
//
|
||||
//
|
||||
// 🌟 This was machine generated. Do not edit. 🌟
|
||||
{
|
||||
"frames": [
|
||||
{
|
||||
"schema": {
|
||||
"name": "Query results",
|
||||
"meta": {
|
||||
"type": "search-results",
|
||||
"custom": {
|
||||
"count": 0
|
||||
}
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "kind",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "uid",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "panel_type",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
},
|
||||
"config": {
|
||||
"links": [
|
||||
{
|
||||
"title": "link",
|
||||
"url": "${__value.text}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ds_uid",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -24,6 +24,7 @@ type EntityType string
|
||||
|
||||
const (
|
||||
EntityTypeDashboard EntityType = "dashboard"
|
||||
EntityTypeFolder EntityType = "folder"
|
||||
)
|
||||
|
||||
// CreateDatabaseEntityId creates entityId for entities stored in the existing SQL tables
|
||||
@ -51,6 +52,8 @@ type SaveEventCmd struct {
|
||||
EventType EntityEventType
|
||||
}
|
||||
|
||||
type EventHandler func(ctx context.Context, e *EntityEvent) error
|
||||
|
||||
// EntityEventsService is a temporary solution to support change notifications in an HA setup
|
||||
// With this service each system can query for any events that have happened since a fixed time
|
||||
//go:generate mockery --name EntityEventsService --structname MockEntityEventsService --inpackage --filename entity_events_mock.go
|
||||
@ -60,6 +63,7 @@ type EntityEventsService interface {
|
||||
SaveEvent(ctx context.Context, cmd SaveEventCmd) error
|
||||
GetLastEvent(ctx context.Context) (*EntityEvent, error)
|
||||
GetAllEventsAfter(ctx context.Context, id int64) ([]*EntityEvent, error)
|
||||
OnEvent(handler EventHandler)
|
||||
|
||||
deleteEventsOlderThan(ctx context.Context, duration time.Duration) error
|
||||
}
|
||||
@ -70,27 +74,48 @@ func ProvideEntityEventsService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, f
|
||||
}
|
||||
|
||||
return &entityEventService{
|
||||
sql: sqlStore,
|
||||
features: features,
|
||||
log: log.New("entity-events"),
|
||||
sql: sqlStore,
|
||||
features: features,
|
||||
log: log.New("entity-events"),
|
||||
eventHandlers: make([]EventHandler, 0),
|
||||
}
|
||||
}
|
||||
|
||||
type entityEventService struct {
|
||||
sql *sqlstore.SQLStore
|
||||
log log.Logger
|
||||
features featuremgmt.FeatureToggles
|
||||
sql *sqlstore.SQLStore
|
||||
log log.Logger
|
||||
features featuremgmt.FeatureToggles
|
||||
eventHandlers []EventHandler
|
||||
}
|
||||
|
||||
func (e *entityEventService) SaveEvent(ctx context.Context, cmd SaveEventCmd) error {
|
||||
return e.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
_, err := sess.Insert(&EntityEvent{
|
||||
EventType: cmd.EventType,
|
||||
EntityId: cmd.EntityId,
|
||||
Created: time.Now().Unix(),
|
||||
})
|
||||
entityEvent := &EntityEvent{
|
||||
EventType: cmd.EventType,
|
||||
EntityId: cmd.EntityId,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
err := e.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
_, err := sess.Insert(entityEvent)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.broadcastEvent(ctx, entityEvent)
|
||||
}
|
||||
|
||||
func (e *entityEventService) broadcastEvent(ctx context.Context, event *EntityEvent) error {
|
||||
for _, h := range e.eventHandlers {
|
||||
err := h(ctx, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *entityEventService) OnEvent(handler EventHandler) {
|
||||
e.eventHandlers = append(e.eventHandlers, handler)
|
||||
}
|
||||
|
||||
func (e *entityEventService) GetLastEvent(ctx context.Context) (*EntityEvent, error) {
|
||||
@ -164,6 +189,9 @@ func (d dummyEntityEventsService) SaveEvent(ctx context.Context, cmd SaveEventCm
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d dummyEntityEventsService) OnEvent(handler EventHandler) {
|
||||
}
|
||||
|
||||
func (d dummyEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEvent, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.10.6. DO NOT EDIT.
|
||||
// Code generated by mockery v2.12.3. DO NOT EDIT.
|
||||
|
||||
package store
|
||||
|
||||
@ -74,6 +74,11 @@ func (_m *MockEntityEventsService) IsDisabled() bool {
|
||||
return r0
|
||||
}
|
||||
|
||||
// OnEvent provides a mock function with given fields: handler
|
||||
func (_m *MockEntityEventsService) OnEvent(handler EventHandler) {
|
||||
_m.Called(handler)
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: ctx
|
||||
func (_m *MockEntityEventsService) Run(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
@ -115,3 +120,18 @@ func (_m *MockEntityEventsService) deleteEventsOlderThan(ctx context.Context, du
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type NewMockEntityEventsServiceT interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewMockEntityEventsService creates a new instance of MockEntityEventsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockEntityEventsService(t NewMockEntityEventsServiceT) *MockEntityEventsService {
|
||||
mock := &MockEntityEventsService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user