From b2408dd7c5860fbb3fad8360a72862e88c2859b6 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 18 Oct 2022 19:48:20 -0600 Subject: [PATCH] Publicdasboards: Add annotations support (#56413) adds annotations support for public dashboards --- .../src/raw/dashboard/x/dashboard.gen.ts | 7 +- pkg/coremodel/dashboard/coremodel.cue | 9 +- pkg/coremodel/dashboard/dashboard_gen.go | 25 +- pkg/services/annotations/annotations.go | 1 + .../annotations_repository_mock.go | 111 ++++++ pkg/services/publicdashboards/api/api.go | 16 + pkg/services/publicdashboards/api/api_test.go | 55 ++- .../publicdashboards/models/models.go | 28 ++ .../public_dashboard_service_mock.go | 23 ++ .../publicdashboards/publicdashboard.go | 1 + .../publicdashboards/service/annotations.go | 22 ++ .../publicdashboards/service/service.go | 104 +++++- .../publicdashboards/service/service_test.go | 327 ++++++++++++++++++ .../annotations/executeAnnotationQuery.ts | 1 + .../annotations/standardAnnotationSupport.ts | 17 +- .../PublicDashboardDataSource.test.ts | 53 ++- .../services/PublicDashboardDataSource.ts | 70 +++- .../AnnotationsWorker.test.ts | 2 +- .../DashboardQueryRunner/AnnotationsWorker.ts | 30 +- .../UnifiedAlertStatesWorker.test.ts | 12 +- .../UnifiedAlertStatesWorker.ts | 5 + 21 files changed, 872 insertions(+), 47 deletions(-) create mode 100644 pkg/services/annotations/annotations_repository_mock.go create mode 100644 pkg/services/publicdashboards/service/annotations.go diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts index 267093da15c..50571f93aa5 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts @@ -40,7 +40,12 @@ export interface AnnotationQuery { */ rawQuery?: string; showIn: number; - target?: Record; + target?: { + limit: number; + matchAny: boolean; + tags: Array; + type: string; + }; type: string; } diff --git a/pkg/coremodel/dashboard/coremodel.cue b/pkg/coremodel/dashboard/coremodel.cue index a21fd17acbd..5bda46f63f6 100644 --- a/pkg/coremodel/dashboard/coremodel.cue +++ b/pkg/coremodel/dashboard/coremodel.cue @@ -84,6 +84,13 @@ seqs: [ /////////////////////////////////////// // Definitions (referenced above) are declared below + #AnnotationTarget: { + limit: int64 + matchAny: bool + tags: [...string] + type: string + } + // TODO docs // FROM: AnnotationQuery in grafana-data/src/types/annotations.ts #AnnotationQuery: { @@ -106,7 +113,7 @@ seqs: [ // Query for annotation data. rawQuery?: string @grafanamaturity(NeedsExpertReview) showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview) - target?: #Target @grafanamaturity(NeedsExpertReview) // TODO currently a generic in AnnotationQuery + target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview) } @cuetsy(kind="interface") // FROM: packages/grafana-data/src/types/templateVars.ts diff --git a/pkg/coremodel/dashboard/dashboard_gen.go b/pkg/coremodel/dashboard/dashboard_gen.go index 97080a48a1b..0b1e0768481 100644 --- a/pkg/coremodel/dashboard/dashboard_gen.go +++ b/pkg/coremodel/dashboard/dashboard_gen.go @@ -343,18 +343,21 @@ type AnnotationQuery struct { Name *string `json:"name,omitempty"` // Query for annotation data. - RawQuery *string `json:"rawQuery,omitempty"` - ShowIn int `json:"showIn"` + RawQuery *string `json:"rawQuery,omitempty"` + ShowIn int `json:"showIn"` + Target *AnnotationTarget `json:"target,omitempty"` + Type string `json:"type"` +} - // Schema for panel targets is specified by datasource - // plugins. We use a placeholder definition, which the Go - // schema loader either left open/as-is with the Base - // variant of the Model and Panel families, or filled - // with types derived from plugins in the Instance variant. - // When working directly from CUE, importers can extend this - // type directly to achieve the same effect. - Target *Target `json:"target,omitempty"` - Type string `json:"type"` +// AnnotationTarget is the Go representation of a dashboard.AnnotationTarget. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type AnnotationTarget struct { + Limit int64 `json:"limit"` + MatchAny bool `json:"matchAny"` + Tags []string `json:"tags"` + Type string `json:"type"` } // 0 for no shared crosshair or tooltip (default). diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index b41816def93..446e3a68105 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -13,6 +13,7 @@ var ( ErrBaseTagLimitExceeded = errutil.NewBase(errutil.StatusBadRequest, "annotations.tag-limit-exceeded", errutil.WithPublicMessage("Tags length exceeds the maximum allowed.")) ) +//go:generate mockery --name Repository --structname FakeAnnotationsRepo --inpackage --filename annotations_repository_mock.go type Repository interface { Save(ctx context.Context, item *Item) error Update(ctx context.Context, item *Item) error diff --git a/pkg/services/annotations/annotations_repository_mock.go b/pkg/services/annotations/annotations_repository_mock.go new file mode 100644 index 00000000000..d81766764e6 --- /dev/null +++ b/pkg/services/annotations/annotations_repository_mock.go @@ -0,0 +1,111 @@ +// Code generated by mockery v2.12.1. DO NOT EDIT. + +package annotations + +import ( + context "context" + testing "testing" + + mock "github.com/stretchr/testify/mock" +) + +// FakeAnnotationsRepo is an autogenerated mock type for the Repository type +type FakeAnnotationsRepo struct { + mock.Mock +} + +// Delete provides a mock function with given fields: ctx, params +func (_m *FakeAnnotationsRepo) Delete(ctx context.Context, params *DeleteParams) error { + ret := _m.Called(ctx, params) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *DeleteParams) error); ok { + r0 = rf(ctx, params) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Find provides a mock function with given fields: ctx, query +func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*ItemDTO, error) { + ret := _m.Called(ctx, query) + + var r0 []*ItemDTO + if rf, ok := ret.Get(0).(func(context.Context, *ItemQuery) []*ItemDTO); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*ItemDTO) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *ItemQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FindTags provides a mock function with given fields: ctx, query +func (_m *FakeAnnotationsRepo) FindTags(ctx context.Context, query *TagsQuery) (FindTagsResult, error) { + ret := _m.Called(ctx, query) + + var r0 FindTagsResult + if rf, ok := ret.Get(0).(func(context.Context, *TagsQuery) FindTagsResult); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Get(0).(FindTagsResult) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *TagsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Save provides a mock function with given fields: ctx, item +func (_m *FakeAnnotationsRepo) Save(ctx context.Context, item *Item) error { + ret := _m.Called(ctx, item) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok { + r0 = rf(ctx, item) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, item +func (_m *FakeAnnotationsRepo) Update(ctx context.Context, item *Item) error { + ret := _m.Called(ctx, item) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok { + r0 = rf(ctx, item) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewFakeAnnotationsRepo creates a new instance of FakeAnnotationsRepo. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewFakeAnnotationsRepo(t testing.TB) *FakeAnnotationsRepo { + mock := &FakeAnnotationsRepo{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index 6f7247ba695..50520d78c01 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -63,6 +63,7 @@ func (api *Api) RegisterAPIEndpoints() { // public endpoints api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard)) api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard)) + api.RouteRegister.Get("/api/public/dashboards/:accessToken/annotations", routing.Wrap(api.GetAnnotations)) // List Public Dashboards api.RouteRegister.Get("/api/dashboards/public", middleware.ReqSignedIn, routing.Wrap(api.ListPublicDashboards)) @@ -187,6 +188,21 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response { return toJsonStreamingResponse(api.Features, resp) } +func (api *Api) GetAnnotations(c *models.ReqContext) response.Response { + reqDTO := AnnotationsQueryDTO{ + From: c.QueryInt64("from"), + To: c.QueryInt64("to"), + } + + annotations, err := api.PublicDashboardService.GetAnnotations(c.Req.Context(), reqDTO, web.Params(c.Req)[":accessToken"]) + + if err != nil { + return api.handleError(c.Req.Context(), http.StatusInternalServerError, "error getting public dashboard annotations", err) + } + + return response.JSON(http.StatusOK, annotations) +} + // util to help us unpack dashboard and publicdashboard errors or use default http code and message // we should look to do some future refactoring of these errors as publicdashboard err is the same as a dashboarderr, just defined in a // different package. diff --git a/pkg/services/publicdashboards/api/api_test.go b/pkg/services/publicdashboards/api/api_test.go index 5d21d516b85..73e6b567c1f 100644 --- a/pkg/services/publicdashboards/api/api_test.go +++ b/pkg/services/publicdashboards/api/api_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/dashboards" dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/datasources" @@ -49,6 +50,56 @@ type JsonErrResponse struct { Error string `json:"error"` } +func TestAPIGetAnnotations(t *testing.T) { + testCases := []struct { + Name string + ExpectedHttpResponse int + Annotations []AnnotationEvent + ServiceError error + From string + To string + }{ + { + Name: "will return success when there is no error and to and from are provided", + ExpectedHttpResponse: http.StatusOK, + Annotations: []AnnotationEvent{{Id: 1}}, + ServiceError: nil, + From: "123", + To: "123", + }, + { + Name: "will return 500 when service returns an error", + ExpectedHttpResponse: http.StatusInternalServerError, + Annotations: nil, + ServiceError: errors.New("an error happened"), + From: "123", + To: "123", + }, + } + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.RBACEnabled = false + service := publicdashboards.NewFakePublicDashboardService(t) + service.On("GetAnnotations", mock.Anything, mock.Anything, mock.AnythingOfType("string")). + Return(test.Annotations, test.ServiceError).Once() + testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, anonymousUser) + + path := fmt.Sprintf("/api/public/dashboards/abc123/annotations?from=%s&to=%s", test.From, test.To) + response := callAPI(testServer, http.MethodGet, path, nil, t) + + assert.Equal(t, test.ExpectedHttpResponse, response.Code) + + if test.ExpectedHttpResponse == http.StatusOK { + var items []AnnotationEvent + err := json.Unmarshal(response.Body.Bytes(), &items) + assert.NoError(t, err) + assert.Equal(t, items, test.Annotations) + } + }) + } +} + func TestAPIFeatureFlag(t *testing.T) { testCases := []struct { Name string @@ -630,11 +681,13 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) }, } + annotationsService := annotationstest.NewFakeAnnotationsRepo() + // create public dashboard store := publicdashboardsStore.ProvideStore(db) cfg := setting.NewCfg() cfg.RBACEnabled = false - service := publicdashboardsService.ProvideService(cfg, store, qds) + service := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService) pubdash, err := service.SavePublicDashboardConfig(context.Background(), &user.SignedInUser{}, savePubDashboardCmd) require.NoError(t, err) diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index 79adb594b37..0827ed0ef68 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "github.com/grafana/grafana/pkg/coremodel/dashboard" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) @@ -77,6 +78,28 @@ type PublicDashboard struct { UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at"` } +// Alias the generated type +type DashAnnotation = dashboard.AnnotationQuery + +type AnnotationsDto struct { + Annotations struct { + List []DashAnnotation `json:"list"` + } +} + +type AnnotationEvent struct { + Id int64 `json:"id"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + Tags []string `json:"tags"` + IsRegion bool `json:"isRegion"` + Text string `json:"text"` + Color string `json:"color"` + Time int64 `json:"time"` + TimeEnd int64 `json:"timeEnd"` + Source dashboard.AnnotationQuery `json:"source"` +} + func (pd PublicDashboard) TableName() string { return "dashboard_public" } @@ -135,6 +158,11 @@ type PublicDashboardQueryDTO struct { MaxDataPoints int64 } +type AnnotationsQueryDTO struct { + From int64 + To int64 +} + // // COMMANDS // diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index c7429829950..d68314ceece 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -62,6 +62,29 @@ func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, da return r0 } +// GetAnnotations provides a mock function with given fields: ctx, reqDTO, accessToken +func (_m *FakePublicDashboardService) GetAnnotations(ctx context.Context, reqDTO publicdashboardsmodels.AnnotationsQueryDTO, accessToken string) ([]publicdashboardsmodels.AnnotationEvent, error) { + ret := _m.Called(ctx, reqDTO, accessToken) + + var r0 []publicdashboardsmodels.AnnotationEvent + if rf, ok := ret.Get(0).(func(context.Context, publicdashboardsmodels.AnnotationsQueryDTO, string) []publicdashboardsmodels.AnnotationEvent); ok { + r0 = rf(ctx, reqDTO, accessToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]publicdashboardsmodels.AnnotationEvent) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, publicdashboardsmodels.AnnotationsQueryDTO, string) error); ok { + r1 = rf(ctx, reqDTO, accessToken) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDashboard provides a mock function with given fields: ctx, dashboardUid func (_m *FakePublicDashboardService) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) { ret := _m.Called(ctx, dashboardUid) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index 2631b2409a4..4951583f7b4 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -16,6 +16,7 @@ import ( type Service interface { AccessTokenExists(ctx context.Context, accessToken string) (bool, error) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser + GetAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) diff --git a/pkg/services/publicdashboards/service/annotations.go b/pkg/services/publicdashboards/service/annotations.go new file mode 100644 index 00000000000..87b9e70a084 --- /dev/null +++ b/pkg/services/publicdashboards/service/annotations.go @@ -0,0 +1,22 @@ +package service + +import ( + "encoding/json" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/publicdashboards/models" +) + +func UnmarshalDashboardAnnotations(sj *simplejson.Json) (*models.AnnotationsDto, error) { + bytes, err := sj.MarshalJSON() + if err != nil { + return nil, err + } + dto := &models.AnnotationsDto{} + err = json.Unmarshal(bytes, dto) + if err != nil { + return nil, err + } + + return dto, err +} diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index fec02294e74..3e31dc6ceaf 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -2,13 +2,15 @@ package service import ( "context" - "fmt" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -17,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/grafanads" "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) @@ -29,6 +32,7 @@ type PublicDashboardServiceImpl struct { store publicdashboards.Store intervalCalculator intervalv2.Calculator QueryDataService *query.Service + AnnotationsRepo annotations.Repository } var LogPrefix = "publicdashboards.service" @@ -43,6 +47,7 @@ func ProvideService( cfg *setting.Cfg, store publicdashboards.Store, qds *query.Service, + anno annotations.Repository, ) *PublicDashboardServiceImpl { return &PublicDashboardServiceImpl{ log: log.New(LogPrefix), @@ -50,6 +55,7 @@ func ProvideService( store: store, intervalCalculator: intervalv2.NewCalculator(), QueryDataService: qds, + AnnotationsRepo: anno, } } @@ -252,6 +258,81 @@ func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dash return metricReqDTO, nil } +func (pd *PublicDashboardServiceImpl) GetAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) { + _, dash, err := pd.GetPublicDashboard(ctx, accessToken) + if err != nil { + return nil, err + } + + annoDto, err := UnmarshalDashboardAnnotations(dash.Data) + if err != nil { + return nil, err + } + + anonymousUser := pd.BuildAnonymousUser(ctx, dash) + + uniqueEvents := make(map[int64]AnnotationEvent, 0) + for _, anno := range annoDto.Annotations.List { + // skip annotations that are not enabled or are not a grafana datasource + if !anno.Enable || (*anno.Datasource.Uid != grafanads.DatasourceUID && *anno.Datasource.Uid != grafanads.DatasourceName) { + continue + } + annoQuery := &annotations.ItemQuery{ + From: reqDTO.From, + To: reqDTO.To, + OrgId: dash.OrgId, + DashboardId: dash.Id, + DashboardUid: dash.Uid, + Limit: anno.Target.Limit, + MatchAny: anno.Target.MatchAny, + SignedInUser: anonymousUser, + } + + if anno.Target.Type == "tags" { + annoQuery.DashboardId = 0 + annoQuery.Tags = anno.Target.Tags + } + + annotationItems, err := pd.AnnotationsRepo.Find(ctx, annoQuery) + if err != nil { + return nil, err + } + + for _, item := range annotationItems { + event := AnnotationEvent{ + Id: item.Id, + DashboardId: item.DashboardId, + Tags: item.Tags, + IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd, + Text: item.Text, + Color: *anno.IconColor, + Time: item.Time, + TimeEnd: item.TimeEnd, + Source: anno, + } + + // We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels + // which is only intended for tag and org annotations. + if anno.Type == "dashboard" { + event.PanelId = item.PanelId + } + + // We want events from tag queries to overwrite existing events + _, has := uniqueEvents[event.Id] + if !has || (has && anno.Target.Type == "tags") { + uniqueEvents[event.Id] = event + } + } + } + + var results []AnnotationEvent + for _, result := range uniqueEvents { + results = append(results, result) + } + + return results, nil +} + // buildMetricRequest merges public dashboard parameters with // dashboard and returns a metrics request to be sent to query backend func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) { @@ -282,17 +363,30 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, da func (pd *PublicDashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser { datasourceUids := queries.GetUniqueDashboardDatasourceUids(dashboard.Data) - // Create a temp user with read-only datasource permissions + // Create a user with blank permissions anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} - permissions := make(map[string][]string) + + // Scopes needed for Annotation queries + annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard} + // Need to access all dashboards since tags annotations span across all dashboards + dashboardScopes := []string{dashboards.ScopeDashboardsProvider.GetResourceAllScope()} + + // Scopes needed for datasource queries queryScopes := make([]string, 0) readScopes := make([]string, 0) for _, uid := range datasourceUids { - queryScopes = append(queryScopes, fmt.Sprintf("datasources:uid:%s", uid)) - readScopes = append(readScopes, fmt.Sprintf("datasources:uid:%s", uid)) + scope := datasources.ScopeProvider.GetResourceScopeUID(uid) + queryScopes = append(queryScopes, scope) + readScopes = append(readScopes, scope) } + + // Apply all scopes to the actions we need the user to be able to perform + permissions := make(map[string][]string) permissions[datasources.ActionQuery] = queryScopes permissions[datasources.ActionRead] = readScopes + permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes + permissions[dashboards.ActionDashboardsRead] = dashboardScopes + anonymousUser.Permissions[dashboard.OrgId] = permissions return anonymousUser diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 671f831df11..5d0804f96b1 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -2,13 +2,19 @@ package service import ( "context" + "encoding/json" + "errors" "testing" "time" + dashboard2 "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/publicdashboards/internal" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -35,6 +41,289 @@ func TestLogPrefix(t *testing.T) { assert.Equal(t, LogPrefix, "publicdashboards.service") } +func TestGetAnnotations(t *testing.T) { + t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + config := setting.NewCfg() + tagService := tagimpl.ProvideService(sqlStore, sqlStore.Cfg) + annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, tagService) + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: annotationsRepo, + } + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). + Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, models.NewDashboard("dash1"), nil) + reqDTO := AnnotationsQueryDTO{ + From: 1, + To: 2, + } + dash := models.NewDashboard("testDashboard") + + items, _ := service.GetAnnotations(context.Background(), reqDTO, "abc123") + anonUser := service.BuildAnonymousUser(context.Background(), dash) + + assert.Equal(t, "dashboards:*", anonUser.Permissions[0]["dashboards:read"][0]) + assert.Len(t, items, 0) + }) + + t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { + dash := models.NewDashboard("test") + color := "red" + name := "annoName" + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: nil, + Type: "dashboard", + }, + Type: "dashboard", + } + grafanaTagAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: []string{"tag1"}, + Type: "tags", + }, + } + annos := []DashAnnotation{grafanaAnnotation, grafanaTagAnnotation} + dashboard := AddAnnotationsToDashboard(t, dash, annos) + + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ + { + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{"tag1"}, + TimeEnd: 2, + Time: 2, + Text: "text", + }, + }, nil).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + expected := AnnotationEvent{ + Id: 1, + DashboardId: 1, + PanelId: 0, + Tags: []string{"tag1"}, + IsRegion: false, + Text: "text", + Color: color, + Time: 2, + TimeEnd: 2, + Source: grafanaTagAnnotation, + } + require.NoError(t, err) + assert.Len(t, items, 1) + assert.Equal(t, expected, items[0]) + }) + + t.Run("Test panelId set to zero when annotation event is for a tags query", func(t *testing.T) { + dash := models.NewDashboard("test") + color := "red" + name := "annoName" + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: []string{"tag1"}, + Type: "tags", + }, + } + annos := []DashAnnotation{grafanaAnnotation} + dashboard := AddAnnotationsToDashboard(t, dash, annos) + + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ + { + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{}, + TimeEnd: 1, + Time: 2, + Text: "text", + }, + }, nil).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + expected := AnnotationEvent{ + Id: 1, + DashboardId: 1, + PanelId: 0, + Tags: []string{}, + IsRegion: true, + Text: "text", + Color: color, + Time: 2, + TimeEnd: 1, + Source: grafanaAnnotation, + } + require.NoError(t, err) + assert.Len(t, items, 1) + assert.Equal(t, expected, items[0]) + }) + + t.Run("Test can get grafana annotations and will skip annotation queries and disabled annotations", func(t *testing.T) { + dash := models.NewDashboard("test") + color := "red" + name := "annoName" + disabledGrafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: false, + Name: &name, + IconColor: &color, + } + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: true, + Tags: nil, + Type: "dashboard", + }, + Type: "dashboard", + } + queryAnnotation := DashAnnotation{ + Datasource: CreateDatasource("prometheus", "abc123"), + Enable: true, + Name: &name, + } + annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation} + dashboard := AddAnnotationsToDashboard(t, dash, annos) + + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ + { + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{}, + TimeEnd: 1, + Time: 2, + Text: "text", + }, + }, nil).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + expected := AnnotationEvent{ + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{}, + IsRegion: true, + Text: "text", + Color: color, + Time: 2, + TimeEnd: 1, + Source: grafanaAnnotation, + } + require.NoError(t, err) + assert.Len(t, items, 1) + assert.Equal(t, expected, items[0]) + }) + + t.Run("test will return nothing when dashboard has no annotations", func(t *testing.T) { + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + dashboard := models.NewDashboard("dashWithNoAnnotations") + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + require.NoError(t, err) + assert.Empty(t, items) + }) + + t.Run("test will error when annotations repo returns an error", func(t *testing.T) { + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + dash := models.NewDashboard("test") + color := "red" + name := "annoName" + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: []string{"tag1"}, + Type: "tags", + }, + } + annos := []DashAnnotation{grafanaAnnotation} + dash = AddAnnotationsToDashboard(t, dash, annos) + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dash, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return(nil, errors.New("failed")).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + require.Error(t, err) + require.Nil(t, items) + }) +} + func TestGetPublicDashboard(t *testing.T) { type storeResp struct { pd *PublicDashboard @@ -374,12 +663,20 @@ func TestBuildAnonymousUser(t *testing.T) { t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { user := service.BuildAnonymousUser(context.Background(), dashboard) + require.Equal(t, dashboard.OrgId, user.OrgID) require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:query"][0]) require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:query"][1]) require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:read"][0]) require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:read"][1]) }) + t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) { + user := service.BuildAnonymousUser(context.Background(), dashboard) + + require.Equal(t, dashboard.OrgId, user.OrgID) + require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0]) + require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) + }) } func TestGetMetricRequest(t *testing.T) { @@ -863,3 +1160,33 @@ func TestDashboardEnabledChanged(t *testing.T) { assert.True(t, publicDashboardIsEnabledChanged(&PublicDashboard{IsEnabled: false}, &PublicDashboard{IsEnabled: true})) }) } + +func CreateDatasource(dsType string, uid string) struct { + Type *string `json:"type,omitempty"` + Uid *string `json:"uid,omitempty"` +} { + return struct { + Type *string `json:"type,omitempty"` + Uid *string `json:"uid,omitempty"` + }{ + Type: &dsType, + Uid: &uid, + } +} + +func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard { + type annotationsDto struct { + List []DashAnnotation `json:"list"` + } + annos := annotationsDto{} + annos.List = annotations + annoJSON, err := json.Marshal(annos) + require.NoError(t, err) + + dashAnnos, err := simplejson.NewJson(annoJSON) + require.NoError(t, err) + + dash.Data.Set("annotations", dashAnnos) + + return dash +} diff --git a/public/app/features/annotations/executeAnnotationQuery.ts b/public/app/features/annotations/executeAnnotationQuery.ts index 7eab14c8cb5..df9a7e0365c 100644 --- a/public/app/features/annotations/executeAnnotationQuery.ts +++ b/public/app/features/annotations/executeAnnotationQuery.ts @@ -53,6 +53,7 @@ export function executeAnnotationQuery( scopedVars, ...interval, app: CoreApp.Dashboard, + publicDashboardAccessToken: options.dashboard.meta.publicDashboardAccessToken, timezone: options.dashboard.timezone, diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts index a3dac840695..d8997eb6df8 100644 --- a/public/app/features/annotations/standardAnnotationSupport.ts +++ b/public/app/features/annotations/standardAnnotationSupport.ts @@ -16,6 +16,7 @@ import { KeyValue, standardTransformers, } from '@grafana/data'; +import { config } from 'app/core/config'; export const standardAnnotationSupport: AnnotationSupport = { /** @@ -112,9 +113,22 @@ export const annotationEventNames: AnnotationFieldInfo[] = [ }, ]; +export const publicDashboardEventNames: AnnotationFieldInfo[] = [ + { + key: 'color', + }, + { + key: 'isRegion', + }, + { + key: 'source', + }, +]; + // Given legacy infrastructure, alert events are passed though the same annotation // pipeline, but include fields that should not be exposed generally const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [ + ...(config.isPublicDashboardView ? publicDashboardEventNames : []), ...annotationEventNames, { key: 'userId' }, { key: 'login' }, @@ -185,7 +199,8 @@ export function getAnnotationsFromData( } if (!hasTime || !hasText) { - return []; // throw an error? + console.error('Cannot process annotation fields. No time or text present.'); + return []; } // Add each value to the string diff --git a/public/app/features/dashboard/services/PublicDashboardDataSource.test.ts b/public/app/features/dashboard/services/PublicDashboardDataSource.test.ts index f44931ba773..c542b91f496 100644 --- a/public/app/features/dashboard/services/PublicDashboardDataSource.test.ts +++ b/public/app/features/dashboard/services/PublicDashboardDataSource.test.ts @@ -1,9 +1,12 @@ import { of } from 'rxjs'; -import { DataQueryRequest, DataSourceInstanceSettings, DataSourceRef } from '@grafana/data'; +import { DataQueryRequest, DataSourceInstanceSettings, DataSourceRef, TimeRange } from '@grafana/data'; import { BackendSrvRequest, BackendSrv, DataSourceWithBackend } from '@grafana/runtime'; +import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; +import { GRAFANA_DATASOURCE_NAME } from '../../alerting/unified/utils/datasource'; + import { PublicDashboardDataSource, PUBLIC_DATASOURCE, DEFAULT_INTERVAL } from './PublicDashboardDataSource'; const mockDatasourceRequest = jest.fn(); @@ -12,6 +15,9 @@ const backendSrv = { fetch: (options: BackendSrvRequest) => { return of(mockDatasourceRequest(options)); }, + get: (url: string, options?: Partial) => { + return mockDatasourceRequest(url, options); + }, } as unknown as BackendSrv; jest.mock('@grafana/runtime', () => ({ @@ -25,7 +31,50 @@ jest.mock('@grafana/runtime', () => ({ })); describe('PublicDashboardDatasource', () => { - test('Fetches results from the pubdash query endpoint', () => { + test('will add annotation query type to annotations', () => { + const ds = new PublicDashboardDataSource('public'); + const annotationQuery = { + enable: true, + name: 'someName', + iconColor: 'red', + }; + + // @ts-ignore + const annotation = ds?.annotations.prepareQuery(annotationQuery); + + expect(annotation?.queryType).toEqual(GrafanaQueryType.Annotations); + }); + + test('fetches results from the pubdash annotations endpoint when it is an annotation query', async () => { + mockDatasourceRequest.mockReset(); + mockDatasourceRequest.mockReturnValue(Promise.resolve([])); + + const ds = new PublicDashboardDataSource('public'); + const panelId = 1; + const publicDashboardAccessToken = 'abc123'; + + await ds.query({ + maxDataPoints: 10, + intervalMs: 5000, + targets: [ + { + refId: 'A', + datasource: { uid: GRAFANA_DATASOURCE_NAME, type: 'sample' }, + queryType: GrafanaQueryType.Annotations, + }, + ], + panelId, + publicDashboardAccessToken, + range: { from: new Date().toLocaleString(), to: new Date().toLocaleString() } as unknown as TimeRange, + } as DataQueryRequest); + + const mock = mockDatasourceRequest.mock; + + expect(mock.calls.length).toBe(1); + expect(mock.lastCall[0]).toEqual(`/api/public/dashboards/${publicDashboardAccessToken}/annotations`); + }); + + test('fetches results from the pubdash query endpoint when not annotation query', () => { mockDatasourceRequest.mockReset(); mockDatasourceRequest.mockReturnValue(Promise.resolve({})); diff --git a/public/app/features/dashboard/services/PublicDashboardDataSource.ts b/public/app/features/dashboard/services/PublicDashboardDataSource.ts index 1ddecb938be..7d299bba9b0 100644 --- a/public/app/features/dashboard/services/PublicDashboardDataSource.ts +++ b/public/app/features/dashboard/services/PublicDashboardDataSource.ts @@ -1,6 +1,7 @@ -import { catchError, Observable, of, switchMap } from 'rxjs'; +import { catchError, from, Observable, of, switchMap } from 'rxjs'; import { + AnnotationQuery, DataQuery, DataQueryRequest, DataQueryResponse, @@ -8,10 +9,13 @@ import { DataSourceJsonData, DataSourcePluginMeta, DataSourceRef, + toDataFrame, } from '@grafana/data'; import { BackendDataSourceResponse, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; +import { GrafanaQueryType } from '../../../plugins/datasource/grafana/types'; import { MIXED_DATASOURCE_NAME } from '../../../plugins/datasource/mixed/MixedDataSource'; +import { GRAFANA_DATASOURCE_NAME } from '../../alerting/unified/utils/datasource'; export const PUBLIC_DATASOURCE = '-- Public --'; export const DEFAULT_INTERVAL = '1min'; @@ -35,6 +39,12 @@ export class PublicDashboardDataSource extends DataSourceApi({ - url: `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`, - method: 'POST', - data: body, - requestId, - }) - .pipe( - switchMap((raw) => { - return of(toDataQueryResponse(raw, queries)); - }), - catchError((err) => { - return of(toDataQueryResponse(err)); + // Its a datasource query + else { + const body: any = { intervalMs, maxDataPoints }; + + return getBackendSrv() + .fetch({ + url: `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`, + method: 'POST', + data: body, + requestId, }) - ); + .pipe( + switchMap((raw) => { + return of(toDataQueryResponse(raw, queries)); + }), + catchError((err) => { + return of(toDataQueryResponse(err)); + }) + ); + } + } + + async getAnnotations(request: DataQueryRequest): Promise { + const { + publicDashboardAccessToken: accessToken, + range: { to, from }, + } = request; + + const params = { + from: from.valueOf(), + to: to.valueOf(), + }; + const annotations = await getBackendSrv().get(`/api/public/dashboards/${accessToken}/annotations`, params); + + return { data: [toDataFrame(annotations)] }; } testDatasource(): Promise { diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts index f97bfce2082..87cd6497051 100644 --- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts +++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts @@ -91,7 +91,7 @@ describe('AnnotationsWorker', () => { const options = getDefaultOptions(); options.dashboard.meta.publicDashboardAccessToken = 'accessTokenString'; - expect(worker.canWork(options)).toBe(false); + expect(worker.canWork(options)).toBe(true); }); }); diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts index df533b0772a..11ddb34a344 100644 --- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts +++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.ts @@ -6,6 +6,7 @@ import { AnnotationQuery, DataSourceApi } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { AnnotationQueryFinished, AnnotationQueryStarted } from '../../../../types/events'; +import { PUBLIC_DATASOURCE, PublicDashboardDataSource } from '../../../dashboard/services/PublicDashboardDataSource'; import { DashboardModel } from '../../../dashboard/state'; import { AnnotationsQueryRunner } from './AnnotationsQueryRunner'; @@ -29,8 +30,8 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker { canWork({ dashboard }: DashboardQueryRunnerOptions): boolean { const annotations = dashboard.annotations.list.find(AnnotationsWorker.getAnnotationsToProcessFilter); - // We shouldn't return annotations for public dashboards v1 - return Boolean(annotations) && !this.publicDashboardViewMode(dashboard); + + return Boolean(annotations); } work(options: DashboardQueryRunnerOptions): Observable { @@ -39,11 +40,22 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker { } const { dashboard, range } = options; - const annotations = dashboard.annotations.list.filter(AnnotationsWorker.getAnnotationsToProcessFilter); + let annotations = dashboard.annotations.list.filter(AnnotationsWorker.getAnnotationsToProcessFilter); + // We only want to create a single PublicDashboardDatasource. This will get all annotations in one request. + if (dashboard.meta.publicDashboardAccessToken && annotations.length > 0) { + annotations = [annotations[0]]; + } const observables = annotations.map((annotation) => { - const datasourceObservable = from(getDataSourceSrv().get(annotation.datasource)).pipe( - catchError(handleDatasourceSrvError) // because of the reduce all observables need to be completed, so an erroneous observable wont do - ); + let datasourceObservable; + if (dashboard.meta.publicDashboardAccessToken !== '') { + const pubdashDatasource = new PublicDashboardDataSource(PUBLIC_DATASOURCE); + datasourceObservable = of(pubdashDatasource).pipe(catchError(handleDatasourceSrvError)); + } else { + datasourceObservable = from(getDataSourceSrv().get(annotation.datasource)).pipe( + catchError(handleDatasourceSrvError) // because of the reduce all observables need to be completed, so an erroneous observable wont do + ); + } + return datasourceObservable.pipe( mergeMap((datasource?: DataSourceApi) => { const runner = this.runners.find((r) => r.canRun(datasource)); @@ -65,7 +77,11 @@ export class AnnotationsWorker implements DashboardQueryRunnerWorker { annotation.snapshotData = cloneDeep(results); } // translate result - return translateQueryResult(annotation, results); + if (dashboard.meta.publicDashboardAccessToken) { + return results; + } else { + return translateQueryResult(annotation, results); + } }), finalize(() => { dashboard.events.publish(new AnnotationQueryFinished(annotation)); diff --git a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts index 744885d9526..f9f9e2fd81a 100644 --- a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts +++ b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.test.ts @@ -19,7 +19,7 @@ jest.mock('@grafana/runtime', () => ({ })); function getDefaultOptions(): DashboardQueryRunnerOptions { - const dashboard: any = { id: 'an id', uid: 'a uid' }; + const dashboard: any = { id: 'an id', uid: 'a uid', meta: { publicDashboardAccessToken: '' } }; const range = getDefaultTimeRange(); return { dashboard, range }; @@ -49,6 +49,15 @@ describe('UnifiedAlertStatesWorker', () => { }); }); + describe('when canWork is called on a public dashboard view', () => { + it('then it should return false', () => { + const options = getDefaultOptions(); + options.dashboard.meta.publicDashboardAccessToken = 'abc123'; + + expect(worker.canWork(options)).toBe(false); + }); + }); + describe('when canWork is called with no dashboard id', () => { it('then it should return false', () => { const dashboard: any = {}; @@ -114,6 +123,7 @@ describe('UnifiedAlertStatesWorker', () => { ...overrides, }; } + it('then it should return the correct results', async () => { const getResults: PromRulesResponse = { status: 'success', diff --git a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts index 335b1cd20ca..29d2cb02fc0 100644 --- a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts +++ b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts @@ -23,6 +23,11 @@ export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker { return false; } + // Cannot fetch rules while on a public dashboard since it's unauthenticated + if (dashboard.meta.publicDashboardAccessToken) { + return false; + } + if (range.raw.to !== 'now') { return false; }