diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 4759e5ac088..3b118ec2efb 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -484,7 +484,7 @@ export interface DataQueryRequest { panelId?: number; dashboardId?: number; // Temporary prop for public dashboards, to be replaced by publicAccessKey - publicDashboardUid?: string; + publicDashboardAccessToken?: string; // Request Timing startTime: number; diff --git a/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts b/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts index 5c4c6d0bd68..c86ee702e7c 100644 --- a/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts +++ b/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts @@ -30,19 +30,21 @@ describe('PublicDashboardDatasource', () => { const ds = new PublicDashboardDataSource(); const panelId = 1; - const publicDashboardUid = 'abc123'; + const publicDashboardAccessToken = 'abc123'; ds.query({ maxDataPoints: 10, intervalMs: 5000, targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }], panelId, - publicDashboardUid, + publicDashboardAccessToken, } as DataQueryRequest); const mock = mockDatasourceRequest.mock; expect(mock.calls.length).toBe(1); - expect(mock.lastCall[0].url).toEqual(`/api/public/dashboards/${publicDashboardUid}/panels/${panelId}/query`); + expect(mock.lastCall[0].url).toEqual( + `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query` + ); }); }); diff --git a/pkg/api/api.go b/pkg/api/api.go index 23624135aeb..52d06fe9ff0 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -89,11 +89,6 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/a/:id/*", reqSignedIn, hs.Index) // App Root Page r.Get("/a/:id", reqSignedIn, hs.Index) - //pubdash - if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { - r.Get("/public-dashboards/:uid", middleware.SetPublicDashboardFlag(), hs.Index) - } - r.Get("/d/:uid/:slug", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index) r.Get("/d/:uid", reqSignedIn, redirectFromLegacyPanelEditURL, hs.Index) r.Get("/dashboard/script/*", reqSignedIn, hs.Index) @@ -612,8 +607,9 @@ func (hs *HTTPServer) registerRoutes() { // Public API if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { - r.Get("/api/public/dashboards/:uid", routing.Wrap(hs.GetPublicDashboard)) - r.Post("/api/public/dashboards/:uid/panels/:panelId/query", routing.Wrap(hs.QueryPublicDashboard)) + r.Get("/public-dashboards/:accessToken", middleware.SetPublicDashboardFlag(), hs.Index) + r.Get("/api/public/dashboards/:accessToken", routing.Wrap(hs.GetPublicDashboard)) + r.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(hs.QueryPublicDashboard)) } // Frontend logs diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 6b5aa172c91..49d6f293f2a 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -139,7 +139,6 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { Url: dash.GetUrl(), FolderTitle: "General", AnnotationsPermissions: annotationPermissions, - IsPublic: dash.IsPublic, } // lookup folder title diff --git a/pkg/api/dashboard_public.go b/pkg/api/dashboard_public.go index c567fae44e0..5399f21a722 100644 --- a/pkg/api/dashboard_public.go +++ b/pkg/api/dashboard_public.go @@ -2,40 +2,42 @@ package api import ( "errors" + "fmt" "net/http" "strconv" + "strings" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/web" ) // gets public dashboard func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response { - publicDashboardUid := web.Params(c.Req)[":uid"] + accessToken := web.Params(c.Req)[":accessToken"] - dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), publicDashboardUid) + dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), accessToken) if err != nil { return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err) } meta := dtos.DashboardMeta{ - Slug: dash.Slug, - Type: models.DashTypeDB, - CanStar: false, - CanSave: false, - CanEdit: false, - CanAdmin: false, - CanDelete: false, - Created: dash.Created, - Updated: dash.Updated, - Version: dash.Version, - IsFolder: false, - FolderId: dash.FolderId, - IsPublic: dash.IsPublic, - PublicDashboardUid: publicDashboardUid, + Slug: dash.Slug, + Type: models.DashTypeDB, + CanStar: false, + CanSave: false, + CanEdit: false, + CanAdmin: false, + CanDelete: false, + Created: dash.Created, + Updated: dash.Updated, + Version: dash.Version, + IsFolder: false, + FolderId: dash.FolderId, + PublicDashboardAccessToken: accessToken, } dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data} @@ -54,43 +56,72 @@ func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Re // sets public dashboard configuration for dashboard func (hs *HTTPServer) SavePublicDashboardConfig(c *models.ReqContext) response.Response { - pdc := &models.PublicDashboardConfig{} - if err := web.Bind(c.Req, pdc); err != nil { + pubdash := &models.PublicDashboard{} + if err := web.Bind(c.Req, pubdash); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } + // Always set the org id to the current auth session orgId + pubdash.OrgId = c.OrgId + dto := dashboards.SavePublicDashboardConfigDTO{ - OrgId: c.OrgId, - DashboardUid: web.Params(c.Req)[":uid"], - PublicDashboardConfig: pdc, + OrgId: c.OrgId, + DashboardUid: web.Params(c.Req)[":uid"], + UserId: c.UserId, + PublicDashboard: pubdash, } - pdc, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) + pubdash, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) if err != nil { return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err) } - return response.JSON(http.StatusOK, pdc) + return response.JSON(http.StatusOK, pubdash) } // QueryPublicDashboard returns all results for a given panel on a public dashboard -// POST /api/public/dashboard/:uid/panels/:panelId/query +// POST /api/public/dashboard/:accessToken/panels/:panelId/query func (hs *HTTPServer) QueryPublicDashboard(c *models.ReqContext) response.Response { panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "invalid panel ID", err) } + dashboard, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"]) + if err != nil { + return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err) + } + + publicDashboard, err := hs.dashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid) + if err != nil { + return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err) + } + reqDTO, err := hs.dashboardService.BuildPublicDashboardMetricRequest( c.Req.Context(), - web.Params(c.Req)[":uid"], + dashboard, + publicDashboard, panelId, ) if err != nil { return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err) } - resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), nil, c.SkipCache, reqDTO, true) + // Get all needed datasource UIDs from queries + var uids []string + for _, query := range reqDTO.Queries { + uids = append(uids, query.Get("datasource").Get("uid").MustString()) + } + + // Create a temp user with read-only datasource permissions + anonymousUser := &models.SignedInUser{OrgId: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} + permissions := make(map[string][]string) + datasourceScope := fmt.Sprintf("datasources:uid:%s", strings.Join(uids, ",")) + permissions[datasources.ActionQuery] = []string{datasourceScope} + permissions[datasources.ActionRead] = []string{datasourceScope} + anonymousUser.Permissions[dashboard.OrgId] = permissions + + resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true) if err != nil { return hs.handleQueryMetricsError(err) diff --git a/pkg/api/dashboard_public_test.go b/pkg/api/dashboard_public_test.go index 0bef1e8e42e..f72e37de9a6 100644 --- a/pkg/api/dashboard_public_test.go +++ b/pkg/api/dashboard_public_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -18,10 +19,14 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" @@ -54,39 +59,40 @@ func TestAPIGetPublicDashboard(t *testing.T) { assert.Equal(t, http.StatusNotFound, response.Code) }) - dashboardUid := "dashboard-abcd1234" - pubdashUid := "pubdash-abcd1234" + DashboardUid := "dashboard-abcd1234" + token, err := uuid.NewV4() + require.NoError(t, err) + accessToken := fmt.Sprintf("%x", token) testCases := []struct { - name string - uid string - expectedHttpResponse int + Name string + AccessToken string + ExpectedHttpResponse int publicDashboardResult *models.Dashboard publicDashboardErr error }{ { - name: "It gets a public dashboard", - uid: pubdashUid, - expectedHttpResponse: http.StatusOK, + Name: "It gets a public dashboard", + AccessToken: accessToken, + ExpectedHttpResponse: http.StatusOK, publicDashboardResult: &models.Dashboard{ Data: simplejson.NewFromAny(map[string]interface{}{ - "Uid": dashboardUid, + "Uid": DashboardUid, }), - IsPublic: true, }, publicDashboardErr: nil, }, { - name: "It should return 404 if isPublicDashboard is false", - uid: pubdashUid, - expectedHttpResponse: http.StatusNotFound, + Name: "It should return 404 if isPublicDashboard is false", + AccessToken: accessToken, + ExpectedHttpResponse: http.StatusNotFound, publicDashboardResult: nil, publicDashboardErr: models.ErrPublicDashboardNotFound, }, } for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). @@ -97,20 +103,19 @@ func TestAPIGetPublicDashboard(t *testing.T) { response := callAPI( sc.server, http.MethodGet, - fmt.Sprintf("/api/public/dashboards/%v", test.uid), + fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), nil, t, ) - assert.Equal(t, test.expectedHttpResponse, response.Code) + assert.Equal(t, test.ExpectedHttpResponse, response.Code) if test.publicDashboardErr == nil { var dashResp dtos.DashboardFullWithMeta err := json.Unmarshal(response.Body.Bytes(), &dashResp) require.NoError(t, err) - assert.Equal(t, dashboardUid, dashResp.Dashboard.Get("Uid").MustString()) - assert.Equal(t, true, dashResp.Meta.IsPublic) + assert.Equal(t, DashboardUid, dashResp.Dashboard.Get("Uid").MustString()) assert.Equal(t, false, dashResp.Meta.CanEdit) assert.Equal(t, false, dashResp.Meta.CanDelete) assert.Equal(t, false, dashResp.Meta.CanSave) @@ -127,44 +132,44 @@ func TestAPIGetPublicDashboard(t *testing.T) { } func TestAPIGetPublicDashboardConfig(t *testing.T) { - pdc := &models.PublicDashboardConfig{IsPublic: true} + pubdash := &models.PublicDashboard{IsEnabled: true} testCases := []struct { - name string - dashboardUid string - expectedHttpResponse int - publicDashboardConfigResult *models.PublicDashboardConfig - publicDashboardConfigError error + Name string + DashboardUid string + ExpectedHttpResponse int + PublicDashboardResult *models.PublicDashboard + PublicDashboardError error }{ { - name: "retrieves public dashboard config when dashboard is found", - dashboardUid: "1", - expectedHttpResponse: http.StatusOK, - publicDashboardConfigResult: pdc, - publicDashboardConfigError: nil, + Name: "retrieves public dashboard config when dashboard is found", + DashboardUid: "1", + ExpectedHttpResponse: http.StatusOK, + PublicDashboardResult: pubdash, + PublicDashboardError: nil, }, { - name: "returns 404 when dashboard not found", - dashboardUid: "77777", - expectedHttpResponse: http.StatusNotFound, - publicDashboardConfigResult: nil, - publicDashboardConfigError: models.ErrDashboardNotFound, + Name: "returns 404 when dashboard not found", + DashboardUid: "77777", + ExpectedHttpResponse: http.StatusNotFound, + PublicDashboardResult: nil, + PublicDashboardError: models.ErrDashboardNotFound, }, { - name: "returns 500 when internal server error", - dashboardUid: "1", - expectedHttpResponse: http.StatusInternalServerError, - publicDashboardConfigResult: nil, - publicDashboardConfigError: errors.New("database broken"), + Name: "returns 500 when internal server error", + DashboardUid: "1", + ExpectedHttpResponse: http.StatusInternalServerError, + PublicDashboardResult: nil, + PublicDashboardError: errors.New("database broken"), }, } for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")). - Return(test.publicDashboardConfigResult, test.publicDashboardConfigError) + Return(test.PublicDashboardResult, test.PublicDashboardError) sc.hs.dashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) @@ -176,13 +181,13 @@ func TestAPIGetPublicDashboardConfig(t *testing.T) { t, ) - assert.Equal(t, test.expectedHttpResponse, response.Code) + assert.Equal(t, test.ExpectedHttpResponse, response.Code) if response.Code == http.StatusOK { - var pdcResp models.PublicDashboardConfig + var pdcResp models.PublicDashboard err := json.Unmarshal(response.Body.Bytes(), &pdcResp) require.NoError(t, err) - assert.Equal(t, test.publicDashboardConfigResult, &pdcResp) + assert.Equal(t, test.PublicDashboardResult, &pdcResp) } }) } @@ -190,40 +195,40 @@ func TestAPIGetPublicDashboardConfig(t *testing.T) { func TestApiSavePublicDashboardConfig(t *testing.T) { testCases := []struct { - name string - dashboardUid string - publicDashboardConfig *models.PublicDashboardConfig - expectedHttpResponse int + Name string + DashboardUid string + publicDashboardConfig *models.PublicDashboard + ExpectedHttpResponse int saveDashboardError error }{ { - name: "returns 200 when update persists", - dashboardUid: "1", - publicDashboardConfig: &models.PublicDashboardConfig{IsPublic: true}, - expectedHttpResponse: http.StatusOK, + Name: "returns 200 when update persists", + DashboardUid: "1", + publicDashboardConfig: &models.PublicDashboard{IsEnabled: true}, + ExpectedHttpResponse: http.StatusOK, saveDashboardError: nil, }, { - name: "returns 500 when not persisted", - expectedHttpResponse: http.StatusInternalServerError, - publicDashboardConfig: &models.PublicDashboardConfig{}, + Name: "returns 500 when not persisted", + ExpectedHttpResponse: http.StatusInternalServerError, + publicDashboardConfig: &models.PublicDashboard{}, saveDashboardError: errors.New("backend failed to save"), }, { - name: "returns 404 when dashboard not found", - expectedHttpResponse: http.StatusNotFound, - publicDashboardConfig: &models.PublicDashboardConfig{}, + Name: "returns 404 when dashboard not found", + ExpectedHttpResponse: http.StatusNotFound, + publicDashboardConfig: &models.PublicDashboard{}, saveDashboardError: models.ErrDashboardNotFound, }, } for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")). - Return(&models.PublicDashboardConfig{IsPublic: true}, test.saveDashboardError) + Return(&models.PublicDashboard{IsEnabled: true}, test.saveDashboardError) sc.hs.dashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) @@ -235,7 +240,7 @@ func TestApiSavePublicDashboardConfig(t *testing.T) { t, ) - assert.Equal(t, test.expectedHttpResponse, response.Code) + assert.Equal(t, test.ExpectedHttpResponse, response.Code) // check the result if it's a 200 if response.Code == http.StatusOK { @@ -326,10 +331,14 @@ func TestAPIQueryPublicDashboard(t *testing.T) { t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) { server, fakeDashboardService := setup(true) + fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) + fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil) + fakeDashboardService.On( "BuildPublicDashboardMetricRequest", mock.Anything, - "abc123", + mock.Anything, + mock.Anything, int64(2), ).Return(dtos.MetricRequest{ Queries: []*simplejson.Json{ @@ -385,10 +394,13 @@ func TestAPIQueryPublicDashboard(t *testing.T) { t.Run("Status code is 500 when the query fails", func(t *testing.T) { server, fakeDashboardService := setup(true) + fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) + fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil) fakeDashboardService.On( "BuildPublicDashboardMetricRequest", mock.Anything, - "abc123", + mock.Anything, + mock.Anything, int64(2), ).Return(dtos.MetricRequest{ Queries: []*simplejson.Json{ @@ -422,10 +434,13 @@ func TestAPIQueryPublicDashboard(t *testing.T) { t.Run("Status code is 200 when a panel has queries from multiple datasources", func(t *testing.T) { server, fakeDashboardService := setup(true) + fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) + fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil) fakeDashboardService.On( "BuildPublicDashboardMetricRequest", mock.Anything, - "abc123", + mock.Anything, + mock.Anything, int64(2), ).Return(dtos.MetricRequest{ Queries: []*simplejson.Json{ @@ -505,3 +520,111 @@ func TestAPIQueryPublicDashboard(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) }) } + +func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) { + config := setting.NewCfg() + db := sqlstore.InitTestDB(t) + scenario := setupHTTPServerWithCfgDb(t, false, false, config, db, db, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) + scenario.initCtx.SkipCache = true + cacheService := service.ProvideCacheService(localcache.ProvideService(), db) + qds := query.ProvideService( + nil, + cacheService, + nil, + &fakePluginRequestValidator{}, + &fakeDatasources.FakeDataSourceService{}, + &fakePluginClient{ + QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + resp := backend.Responses{ + "A": backend.DataResponse{ + Frames: []*data.Frame{{}}, + }, + } + return &backend.QueryDataResponse{Responses: resp}, nil + }, + }, + &fakeOAuthTokenService{}, + ) + scenario.hs.queryDataService = qds + + _ = db.AddDataSource(context.Background(), &models.AddDataSourceCommand{ + Uid: "ds1", + OrgId: 1, + Name: "laban", + Type: models.DS_MYSQL, + Access: models.DS_ACCESS_DIRECT, + Url: "http://test", + Database: "site", + ReadOnly: true, + }) + + // Create Dashboard + saveDashboardCmd := models.SaveDashboardCommand{ + OrgId: 1, + FolderId: 1, + IsFolder: false, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test", + "panels": []map[string]interface{}{ + { + "id": 1, + "targets": []map[string]interface{}{ + { + "datasource": map[string]string{ + "type": "mysql", + "uid": "ds1", + }, + "refId": "A", + }, + }, + }, + }, + }), + } + dashboard, _ := scenario.dashboardsStore.SaveDashboard(saveDashboardCmd) + + // Create public dashboard + savePubDashboardCmd := &dashboards.SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + PublicDashboard: &models.PublicDashboard{ + IsEnabled: true, + }, + } + + pubdash, err := scenario.hs.dashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd) + require.NoError(t, err) + + response := callAPI( + scenario.server, + http.MethodPost, + fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), + strings.NewReader(`{}`), + t, + ) + + require.Equal(t, http.StatusOK, response.Code) + bodyBytes, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + require.JSONEq( + t, + `{ + "results": { + "A": { + "frames": [ + { + "data": { + "values": [] + }, + "schema": { + "fields": [] + } + } + ] + } + } + }`, + string(bodyBytes), + ) +} diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index eda14d07081..b133fad596b 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -7,34 +7,33 @@ import ( ) type DashboardMeta struct { - IsStarred bool `json:"isStarred,omitempty"` - IsHome bool `json:"isHome,omitempty"` - IsSnapshot bool `json:"isSnapshot,omitempty"` - Type string `json:"type,omitempty"` - CanSave bool `json:"canSave"` - CanEdit bool `json:"canEdit"` - CanAdmin bool `json:"canAdmin"` - CanStar bool `json:"canStar"` - CanDelete bool `json:"canDelete"` - Slug string `json:"slug"` - Url string `json:"url"` - Expires time.Time `json:"expires"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - UpdatedBy string `json:"updatedBy"` - CreatedBy string `json:"createdBy"` - Version int `json:"version"` - HasAcl bool `json:"hasAcl"` - IsFolder bool `json:"isFolder"` - FolderId int64 `json:"folderId"` - FolderUid string `json:"folderUid"` - FolderTitle string `json:"folderTitle"` - FolderUrl string `json:"folderUrl"` - Provisioned bool `json:"provisioned"` - ProvisionedExternalId string `json:"provisionedExternalId"` - AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"` - IsPublic bool `json:"isPublic"` - PublicDashboardUid string `json:"publicDashboardUid"` + IsStarred bool `json:"isStarred,omitempty"` + IsHome bool `json:"isHome,omitempty"` + IsSnapshot bool `json:"isSnapshot,omitempty"` + Type string `json:"type,omitempty"` + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanAdmin bool `json:"canAdmin"` + CanStar bool `json:"canStar"` + CanDelete bool `json:"canDelete"` + Slug string `json:"slug"` + Url string `json:"url"` + Expires time.Time `json:"expires"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + UpdatedBy string `json:"updatedBy"` + CreatedBy string `json:"createdBy"` + Version int `json:"version"` + HasAcl bool `json:"hasAcl"` + IsFolder bool `json:"isFolder"` + FolderId int64 `json:"folderId"` + FolderUid string `json:"folderUid"` + FolderTitle string `json:"folderTitle"` + FolderUrl string `json:"folderUrl"` + Provisioned bool `json:"provisioned"` + ProvisionedExternalId string `json:"provisionedExternalId"` + AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"` + PublicDashboardAccessToken string `json:"publicDashboardAccessToken"` } type AnnotationPermission struct { Dashboard AnnotationActions `json:"dashboard"` diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 1a68389b262..c10fd39e22c 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -200,7 +200,6 @@ type Dashboard struct { FolderId int64 IsFolder bool HasAcl bool - IsPublic bool Title string Data *simplejson.Json diff --git a/pkg/models/dashboards_public.go b/pkg/models/dashboards_public.go index 42c02eb76c2..a25796b7ede 100644 --- a/pkg/models/dashboards_public.go +++ b/pkg/models/dashboards_public.go @@ -1,8 +1,18 @@ package models +import ( + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" +) + var ( ErrPublicDashboardFailedGenerateUniqueUid = DashboardErr{ - Reason: "Failed to generate unique dashboard id", + Reason: "Failed to generate unique public dashboard id", + StatusCode: 500, + } + ErrPublicDashboardFailedGenerateAccesstoken = DashboardErr{ + Reason: "Failed to public dashboard access token", StatusCode: 500, } ErrPublicDashboardNotFound = DashboardErr{ @@ -21,20 +31,51 @@ var ( } ) -type PublicDashboardConfig struct { - IsPublic bool `json:"isPublic"` - PublicDashboard PublicDashboard `json:"publicDashboard"` -} - type PublicDashboard struct { - Uid string `json:"uid" xorm:"uid"` - DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"` - OrgId int64 `json:"orgId" xorm:"org_id"` - TimeSettings string `json:"timeSettings" xorm:"time_settings"` + Uid string `json:"uid" xorm:"uid"` + DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"` + OrgId int64 `json:"-" xorm:"org_id"` // Don't ever marshal orgId to Json + TimeSettings *simplejson.Json `json:"timeSettings" xorm:"time_settings"` + IsEnabled bool `json:"isEnabled" xorm:"is_enabled"` + AccessToken string `json:"accessToken" xorm:"access_token"` + + CreatedBy int64 `json:"createdBy" xorm:"created_by"` + UpdatedBy int64 `json:"updatedBy" xorm:"updated_by"` + + CreatedAt time.Time `json:"createdAt" xorm:"created_at"` + UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at"` } func (pd PublicDashboard) TableName() string { - return "dashboard_public_config" + return "dashboard_public" +} + +type TimeSettings struct { + From string `json:"from"` + To string `json:"to"` +} + +// build time settings object from json on public dashboard. If empty, use +// defaults on the dashboard +func (pd PublicDashboard) BuildTimeSettings(dashboard *Dashboard) *TimeSettings { + ts := &TimeSettings{ + From: dashboard.Data.GetPath("time", "from").MustString(), + To: dashboard.Data.GetPath("time", "to").MustString(), + } + + if pd.TimeSettings == nil { + return ts + } + + // merge time settings from public dashboard + to := pd.TimeSettings.Get("to").MustString("") + from := pd.TimeSettings.Get("from").MustString("") + if to != "" && from != "" { + ts.From = from + ts.To = to + } + + return ts } // @@ -42,7 +83,7 @@ func (pd PublicDashboard) TableName() string { // type SavePublicDashboardConfigCommand struct { - DashboardUid string - OrgId int64 - PublicDashboardConfig PublicDashboardConfig + DashboardUid string + OrgId int64 + PublicDashboard PublicDashboard } diff --git a/pkg/models/dashboards_public_test.go b/pkg/models/dashboards_public_test.go new file mode 100644 index 00000000000..f9b18b0b1cd --- /dev/null +++ b/pkg/models/dashboards_public_test.go @@ -0,0 +1,56 @@ +package models + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/stretchr/testify/assert" +) + +func TestPublicDashboardTableName(t *testing.T) { + assert.Equal(t, "dashboard_public", PublicDashboard{}.TableName()) +} + +func TestBuildTimeSettings(t *testing.T) { + var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}}) + testCases := []struct { + name string + dashboard *Dashboard + pubdash *PublicDashboard + timeResult *TimeSettings + }{ + { + name: "should use dashboard time if pubdash time empty", + dashboard: &Dashboard{Data: dashboardData}, + pubdash: &PublicDashboard{}, + timeResult: &TimeSettings{ + From: "now-8", + To: "now", + }, + }, + { + name: "should use dashboard time if pubdash to/from empty", + dashboard: &Dashboard{Data: dashboardData}, + pubdash: &PublicDashboard{}, + timeResult: &TimeSettings{ + From: "now-8", + To: "now", + }, + }, + { + name: "should use pubdash time", + dashboard: &Dashboard{Data: dashboardData}, + pubdash: &PublicDashboard{TimeSettings: simplejson.NewFromAny(map[string]interface{}{"from": "now-12", "to": "now"})}, + timeResult: &TimeSettings{ + From: "now-12", + To: "now", + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.timeResult, test.pubdash.BuildTimeSettings(test.dashboard)) + }) + } +} diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index e2388ae1e62..9ccdc716346 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -10,7 +10,7 @@ import ( //go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go // DashboardService is a service for operating on dashboards. type DashboardService interface { - BuildPublicDashboardMetricRequest(ctx context.Context, publicDashboardUid string, panelId int64) (dtos.MetricRequest, error) + BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) @@ -19,14 +19,14 @@ type DashboardService interface { GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error - GetPublicDashboard(ctx context.Context, publicDashboardUid string) (*models.Dashboard, error) - GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) + GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) + GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) - SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) + SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error } @@ -65,16 +65,18 @@ type Store interface { GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) - GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) - GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) + GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) + GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) + GenerateNewPublicDashboardUid(ctx context.Context) (string, error) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error // SaveAlerts saves dashboard alerts. SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) - SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) + SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) UnprovisionDashboard(ctx context.Context, id int64) error + UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error // ValidateDashboardBeforeSave validates a dashboard before save. ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 0793f2a6089..e486c1126d1 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.12.2. DO NOT EDIT. +// Code generated by mockery v2.12.1. DO NOT EDIT. package dashboards @@ -18,20 +18,20 @@ type FakeDashboardService struct { mock.Mock } -// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, publicDashboardUid, panelId -func (_m *FakeDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, publicDashboardUid string, panelId int64) (dtos.MetricRequest, error) { - ret := _m.Called(ctx, publicDashboardUid, panelId) +// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId +func (_m *FakeDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) { + ret := _m.Called(ctx, dashboard, publicDashboard, panelId) var r0 dtos.MetricRequest - if rf, ok := ret.Get(0).(func(context.Context, string, int64) dtos.MetricRequest); ok { - r0 = rf(ctx, publicDashboardUid, panelId) + if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *models.PublicDashboard, int64) dtos.MetricRequest); ok { + r0 = rf(ctx, dashboard, publicDashboard, panelId) } else { r0 = ret.Get(0).(dtos.MetricRequest) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, publicDashboardUid, panelId) + if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *models.PublicDashboard, int64) error); ok { + r1 = rf(ctx, dashboard, publicDashboard, panelId) } else { r1 = ret.Error(1) } @@ -169,13 +169,13 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models return r0 } -// GetPublicDashboard provides a mock function with given fields: ctx, publicDashboardUid -func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, publicDashboardUid string) (*models.Dashboard, error) { - ret := _m.Called(ctx, publicDashboardUid) +// GetPublicDashboard provides a mock function with given fields: ctx, accessToken +func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { + ret := _m.Called(ctx, accessToken) var r0 *models.Dashboard if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { - r0 = rf(ctx, publicDashboardUid) + r0 = rf(ctx, accessToken) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.Dashboard) @@ -184,7 +184,7 @@ func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, publicDa var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, publicDashboardUid) + r1 = rf(ctx, accessToken) } else { r1 = ret.Error(1) } @@ -193,15 +193,15 @@ func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, publicDa } // GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) { +func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) - var r0 *models.PublicDashboardConfig - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboardConfig); ok { + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok { r0 = rf(ctx, orgId, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboardConfig) + r0 = ret.Get(0).(*models.PublicDashboard) } } @@ -304,15 +304,15 @@ func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDash } // SavePublicDashboardConfig provides a mock function with given fields: ctx, dto -func (_m *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) { +func (_m *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { ret := _m.Called(ctx, dto) - var r0 *models.PublicDashboardConfig - if rf, ok := ret.Get(0).(func(context.Context, *SavePublicDashboardConfigDTO) *models.PublicDashboardConfig); ok { + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, *SavePublicDashboardConfigDTO) *models.PublicDashboard); ok { r0 = rf(ctx, dto) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboardConfig) + r0 = ret.Get(0).(*models.PublicDashboard) } } diff --git a/pkg/services/dashboards/database/database_dashboard_public.go b/pkg/services/dashboards/database/database_dashboard_public.go index d5f1045b92c..9003b879f5e 100644 --- a/pkg/services/dashboards/database/database_dashboard_public.go +++ b/pkg/services/dashboards/database/database_dashboard_public.go @@ -2,7 +2,6 @@ package database import ( "context" - "fmt" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -10,14 +9,14 @@ import ( ) // retrieves public dashboard configuration -func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) { - if uid == "" { +func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) { + if accessToken == "" { return nil, nil, models.ErrPublicDashboardIdentifierNotSet } // get public dashboard - pdRes := &models.PublicDashboard{Uid: uid} - err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + pdRes := &models.PublicDashboard{AccessToken: accessToken} + err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { has, err := sess.Get(pdRes) if err != nil { return err @@ -34,7 +33,7 @@ func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard // find dashboard dashRes := &models.Dashboard{OrgId: pdRes.OrgId, Uid: pdRes.DashboardUid} - err = d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + err = d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { has, err := sess.Get(dashRes) if err != nil { return err @@ -53,44 +52,43 @@ func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard } // generates a new unique uid to retrieve a public dashboard -func generateNewPublicDashboardUid(sess *sqlstore.DBSession) (string, error) { - for i := 0; i < 3; i++ { - uid := util.GenerateShortUID() +func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) { + var uid string - exists, err := sess.Get(&models.PublicDashboard{Uid: uid}) - if err != nil { - return "", err + err := d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + for i := 0; i < 3; i++ { + uid = util.GenerateShortUID() + + exists, err := sess.Get(&models.PublicDashboard{Uid: uid}) + if err != nil { + return err + } + + if !exists { + return nil + } } - if !exists { - return uid, nil - } + return models.ErrPublicDashboardFailedGenerateUniqueUid + }) + + if err != nil { + return "", err } - return "", models.ErrPublicDashboardFailedGenerateUniqueUid + return uid, nil } // retrieves public dashboard configuration -func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) { +func (d *DashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { if dashboardUid == "" { return nil, models.ErrDashboardIdentifierNotSet } - // get dashboard and publicDashboard - dashRes := &models.Dashboard{OrgId: orgId, Uid: dashboardUid} pdRes := &models.PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid} - err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - // dashboard - has, err := sess.Get(dashRes) - if err != nil { - return err - } - if !has { - return models.ErrDashboardNotFound - } - + err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { // publicDashboard - _, err = sess.Get(pdRes) + _, err := sess.Get(pdRes) if err != nil { return err } @@ -102,46 +100,13 @@ func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid stri return nil, err } - pdc := &models.PublicDashboardConfig{ - IsPublic: dashRes.IsPublic, - PublicDashboard: *pdRes, - } - - return pdc, err + return pdRes, err } // persists public dashboard configuration -func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) { - if len(cmd.PublicDashboardConfig.PublicDashboard.DashboardUid) == 0 { - return nil, models.ErrDashboardIdentifierNotSet - } - - err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - // update isPublic on dashboard entry - affectedRowCount, err := sess.Table("dashboard").Where("org_id = ? AND uid = ?", cmd.OrgId, cmd.DashboardUid).Update(map[string]interface{}{"is_public": cmd.PublicDashboardConfig.IsPublic}) - if err != nil { - return err - } - - if affectedRowCount == 0 { - return models.ErrDashboardNotFound - } - - // update dashboard_public_config - // if we have a uid, public dashboard config exists. delete it otherwise generate a uid - if cmd.PublicDashboardConfig.PublicDashboard.Uid != "" { - if _, err = sess.Exec("DELETE FROM dashboard_public_config WHERE uid=?", cmd.PublicDashboardConfig.PublicDashboard.Uid); err != nil { - return err - } - } else { - uid, err := generateNewPublicDashboardUid(sess) - if err != nil { - return fmt.Errorf("failed to generate UID for public dashboard: %w", err) - } - cmd.PublicDashboardConfig.PublicDashboard.Uid = uid - } - - _, err = sess.Insert(&cmd.PublicDashboardConfig.PublicDashboard) +func (d *DashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) { + err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard) if err != nil { return err } @@ -153,5 +118,19 @@ func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboar return nil, err } - return &cmd.PublicDashboardConfig, nil + return &cmd.PublicDashboard, nil +} + +// updates existing public dashboard configuration +func (d *DashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error { + err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.UseBool("is_enabled").Update(&cmd.PublicDashboard) + if err != nil { + return err + } + + return nil + }) + + return err } diff --git a/pkg/services/dashboards/database/database_dashboard_public_test.go b/pkg/services/dashboards/database/database_dashboard_public_test.go index 4e073cc958b..a897cd19c2b 100644 --- a/pkg/services/dashboards/database/database_dashboard_public_test.go +++ b/pkg/services/dashboards/database/database_dashboard_public_test.go @@ -1,8 +1,11 @@ package database import ( + "context" "testing" + "time" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -11,6 +14,12 @@ import ( "github.com/stretchr/testify/require" ) +// This is what the db sets empty time settings to +var DefaultTimeSettings, _ = simplejson.NewJson([]byte(`{}`)) + +// Default time to pass in with seconds rounded +var DefaultTime = time.Now().UTC().Round(time.Second) + // GetPublicDashboard func TestIntegrationGetPublicDashboard(t *testing.T) { var sqlStore *sqlstore.SQLStore @@ -25,54 +34,55 @@ func TestIntegrationGetPublicDashboard(t *testing.T) { t.Run("returns PublicDashboard and Dashboard", func(t *testing.T) { setup() - pdc, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, - PublicDashboardConfig: models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - Uid: "abc1234", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, - }, + pubdash, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ + PublicDashboard: models.PublicDashboard{ + IsEnabled: true, + Uid: "abc1234", + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + TimeSettings: DefaultTimeSettings, + CreatedAt: DefaultTime, + CreatedBy: 7, + AccessToken: "NOTAREALUUID", }, }) require.NoError(t, err) - pd, d, err := dashboardStore.GetPublicDashboard("abc1234") + pd, d, err := dashboardStore.GetPublicDashboard(context.Background(), "NOTAREALUUID") require.NoError(t, err) - assert.Equal(t, pd, &pdc.PublicDashboard) - assert.Equal(t, d.Uid, pdc.PublicDashboard.DashboardUid) + + assert.Equal(t, pd, pubdash) + assert.Equal(t, d.Uid, pubdash.DashboardUid) }) t.Run("returns ErrPublicDashboardNotFound with empty uid", func(t *testing.T) { setup() - _, _, err := dashboardStore.GetPublicDashboard("") + _, _, err := dashboardStore.GetPublicDashboard(context.Background(), "") require.Error(t, models.ErrPublicDashboardIdentifierNotSet, err) }) t.Run("returns ErrPublicDashboardNotFound when PublicDashboard not found", func(t *testing.T) { setup() - _, _, err := dashboardStore.GetPublicDashboard("zzzzzz") + _, _, err := dashboardStore.GetPublicDashboard(context.Background(), "zzzzzz") require.Error(t, models.ErrPublicDashboardNotFound, err) }) t.Run("returns ErrDashboardNotFound when Dashboard not found", func(t *testing.T) { setup() - _, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ + _, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboardConfig: models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - Uid: "abc1234", - DashboardUid: "nevergonnafindme", - OrgId: savedDashboard.OrgId, - }, + PublicDashboard: models.PublicDashboard{ + IsEnabled: true, + Uid: "abc1234", + DashboardUid: "nevergonnafindme", + OrgId: savedDashboard.OrgId, + CreatedAt: DefaultTime, + CreatedBy: 7, }, }) require.NoError(t, err) - _, _, err = dashboardStore.GetPublicDashboard("abc1234") + _, _, err = dashboardStore.GetPublicDashboard(context.Background(), "abc1234") require.Error(t, models.ErrDashboardNotFound, err) }) } @@ -91,38 +101,40 @@ func TestIntegrationGetPublicDashboardConfig(t *testing.T) { t.Run("returns isPublic and set dashboardUid and orgId", func(t *testing.T) { setup() - pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) - assert.Equal(t, &models.PublicDashboardConfig{IsPublic: false, PublicDashboard: models.PublicDashboard{DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}}, pdc) + assert.Equal(t, &models.PublicDashboard{IsEnabled: false, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}, pubdash) }) t.Run("returns dashboard errDashboardIdentifierNotSet", func(t *testing.T) { setup() - _, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, "") + _, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, "") require.Error(t, models.ErrDashboardIdentifierNotSet, err) }) t.Run("returns isPublic along with public dashboard when exists", func(t *testing.T) { setup() // insert test public dashboard - resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ + resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboardConfig: models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, - TimeSettings: "{from: now, to: then}", - }, + PublicDashboard: models.PublicDashboard{ + IsEnabled: true, + Uid: "pubdash-uid", + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + TimeSettings: DefaultTimeSettings, + CreatedAt: DefaultTime, + CreatedBy: 7, }, }) require.NoError(t, err) - pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) - assert.Equal(t, resp, pdc) + + assert.True(t, assert.ObjectsAreEqualValues(resp, pubdash)) + assert.True(t, assert.ObjectsAreEqual(resp, pubdash)) }) } @@ -142,89 +154,92 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) { t.Run("saves new public dashboard", func(t *testing.T) { setup() - resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ + resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboardConfig: models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, - }, + PublicDashboard: models.PublicDashboard{ + IsEnabled: true, + Uid: "pubdash-uid", + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + TimeSettings: DefaultTimeSettings, + CreatedAt: DefaultTime, + CreatedBy: 7, + AccessToken: "NOTAREALUUID", }, }) require.NoError(t, err) - pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) //verify saved response and queried response are the same - assert.Equal(t, resp, pdc) + assert.Equal(t, resp, pubdash) // verify we have a valid uid - assert.True(t, util.IsValidShortUID(pdc.PublicDashboard.Uid)) + assert.True(t, util.IsValidShortUID(pubdash.Uid)) // verify we didn't update all dashboards - pdc2, err := dashboardStore.GetPublicDashboardConfig(savedDashboard2.OrgId, savedDashboard2.Uid) + pubdash2, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid) require.NoError(t, err) - assert.False(t, pdc2.IsPublic) + assert.False(t, pubdash2.IsEnabled) }) +} - t.Run("returns ErrDashboardIdentifierNotSet", func(t *testing.T) { - setup() - _, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, - PublicDashboardConfig: models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - DashboardUid: "", - OrgId: savedDashboard.OrgId, - }, - }, - }) - require.Error(t, models.ErrDashboardIdentifierNotSet, err) - }) +func TestIntegrationUpdatePublicDashboard(t *testing.T) { + var sqlStore *sqlstore.SQLStore + var dashboardStore *DashboardStore + var savedDashboard *models.Dashboard + + setup := func() { + sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) + dashboardStore = ProvideDashboardStore(sqlStore) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + } - t.Run("overwrites existing public dashboard", func(t *testing.T) { + t.Run("updates an existing dashboard", func(t *testing.T) { setup() - pdUid := util.GenerateShortUID() + pdUid := "asdf1234" - // insert initial record - _, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ + _, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboardConfig: models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - Uid: pdUid, - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, - }, + PublicDashboard: models.PublicDashboard{ + Uid: pdUid, + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + IsEnabled: true, + CreatedAt: DefaultTime, + CreatedBy: 7, + AccessToken: "NOTAREALUUID", }, }) require.NoError(t, err) - // update initial record - resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ + updatedPublicDashboard := models.PublicDashboard{ + Uid: pdUid, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboardConfig: models.PublicDashboardConfig{ - IsPublic: false, - PublicDashboard: models.PublicDashboard{ - Uid: pdUid, - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, - TimeSettings: "{}", - }, - }, + IsEnabled: false, + TimeSettings: simplejson.NewFromAny(map[string]interface{}{"from": "now-8", "to": "now"}), + UpdatedAt: time.Now().UTC().Round(time.Second), + UpdatedBy: 8, + } + // update initial record + err = dashboardStore.UpdatePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + PublicDashboard: updatedPublicDashboard, }) require.NoError(t, err) - pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid) + pdRetrieved, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) - assert.Equal(t, resp, pdc) + + assert.Equal(t, updatedPublicDashboard.UpdatedAt, pdRetrieved.UpdatedAt) + // make sure we're correctly updated IsEnabled because we have to call + // UseBool with xorm + assert.Equal(t, updatedPublicDashboard.IsEnabled, pdRetrieved.IsEnabled) }) } diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 431647e3294..2fedc4abf01 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -16,9 +16,10 @@ type SaveDashboardDTO struct { } type SavePublicDashboardConfigDTO struct { - DashboardUid string - OrgId int64 - PublicDashboardConfig *models.PublicDashboardConfig + DashboardUid string + OrgId int64 + UserId int64 + PublicDashboard *models.PublicDashboard } type DashboardSearchProjection struct { diff --git a/pkg/services/dashboards/service/dashboard_public.go b/pkg/services/dashboards/service/dashboard_public.go index 69dbbf4579f..a0e5c790abe 100644 --- a/pkg/services/dashboards/service/dashboard_public.go +++ b/pkg/services/dashboards/service/dashboard_public.go @@ -2,46 +2,42 @@ package service import ( "context" - "encoding/json" + "fmt" + "time" + "github.com/gofrs/uuid" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" ) -// Gets public dashboard via generated Uid -func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) { - pdc, d, err := dr.dashboardStore.GetPublicDashboard(dashboardUid) +// Gets public dashboard via access token +func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { + pubdash, d, err := dr.dashboardStore.GetPublicDashboard(ctx, accessToken) if err != nil { return nil, err } - if pdc == nil || d == nil { + if pubdash == nil || d == nil { return nil, models.ErrPublicDashboardNotFound } - if !d.IsPublic { + if !pubdash.IsEnabled { return nil, models.ErrPublicDashboardNotFound } - // Replace dashboard time range with pubdash time range - if pdc.TimeSettings != "" { - var pdcTimeSettings map[string]interface{} - err = json.Unmarshal([]byte(pdc.TimeSettings), &pdcTimeSettings) - if err != nil { - return nil, err - } - - d.Data.Set("time", pdcTimeSettings) - } + ts := pubdash.BuildTimeSettings(d) + d.Data.SetPath([]string{"time", "from"}, ts.From) + d.Data.SetPath([]string{"time", "to"}, ts.To) return d, nil } // GetPublicDashboardConfig is a helper method to retrieve the public dashboard configuration for a given dashboard from the database -func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) { - pdc, err := dr.dashboardStore.GetPublicDashboardConfig(orgId, dashboardUid) +func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { + pdc, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, orgId, dashboardUid) if err != nil { return nil, err } @@ -51,53 +47,108 @@ func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, or // SavePublicDashboardConfig is a helper method to persist the sharing config // to the database. It handles validations for sharing config and persistence -func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboardConfig, error) { - cmd := models.SavePublicDashboardConfigCommand{ - DashboardUid: dto.DashboardUid, - OrgId: dto.OrgId, - PublicDashboardConfig: *dto.PublicDashboardConfig, +func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { + if len(dto.DashboardUid) == 0 { + return nil, models.ErrDashboardIdentifierNotSet } - // Eventually we want this to propagate to array of public dashboards - cmd.PublicDashboardConfig.PublicDashboard.OrgId = dto.OrgId - cmd.PublicDashboardConfig.PublicDashboard.DashboardUid = dto.DashboardUid + // set default value for time settings + if dto.PublicDashboard.TimeSettings == nil { + json, err := simplejson.NewJson([]byte("{}")) + if err != nil { + return nil, err + } + dto.PublicDashboard.TimeSettings = json + } - pdc, err := dr.dashboardStore.SavePublicDashboardConfig(cmd) + if dto.PublicDashboard.Uid == "" { + return dr.savePublicDashboardConfig(ctx, dto) + } + + return dr.updatePublicDashboardConfig(ctx, dto) +} + +func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { + uid, err := dr.dashboardStore.GenerateNewPublicDashboardUid(ctx) if err != nil { return nil, err } - return pdc, nil + accessToken, err := GenerateAccessToken() + if err != nil { + return nil, err + } + + cmd := models.SavePublicDashboardConfigCommand{ + DashboardUid: dto.DashboardUid, + OrgId: dto.OrgId, + PublicDashboard: models.PublicDashboard{ + Uid: uid, + DashboardUid: dto.DashboardUid, + OrgId: dto.OrgId, + IsEnabled: dto.PublicDashboard.IsEnabled, + TimeSettings: dto.PublicDashboard.TimeSettings, + CreatedBy: dto.UserId, + CreatedAt: time.Now(), + AccessToken: accessToken, + }, + } + + return dr.dashboardStore.SavePublicDashboardConfig(ctx, cmd) } -func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, publicDashboardUid string, panelId int64) (dtos.MetricRequest, error) { - publicDashboardConfig, dashboard, err := dr.dashboardStore.GetPublicDashboard(publicDashboardUid) - if err != nil { - return dtos.MetricRequest{}, err +func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { + cmd := models.SavePublicDashboardConfigCommand{ + PublicDashboard: models.PublicDashboard{ + Uid: dto.PublicDashboard.Uid, + IsEnabled: dto.PublicDashboard.IsEnabled, + TimeSettings: dto.PublicDashboard.TimeSettings, + UpdatedBy: dto.UserId, + UpdatedAt: time.Now(), + }, } - if !dashboard.IsPublic { + err := dr.dashboardStore.UpdatePublicDashboardConfig(ctx, cmd) + if err != nil { + return nil, err + } + + publicDashboard, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, dto.OrgId, dto.DashboardUid) + if err != nil { + return nil, err + } + + return publicDashboard, nil +} + +// BuildPublicDashboardMetricRequest merges public dashboard parameters with +// dashboard and returns a metrics request to be sent to query backend +func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) { + if !publicDashboard.IsEnabled { return dtos.MetricRequest{}, models.ErrPublicDashboardNotFound } - var timeSettings struct { - From string `json:"from"` - To string `json:"to"` - } - err = json.Unmarshal([]byte(publicDashboardConfig.TimeSettings), &timeSettings) - if err != nil { - return dtos.MetricRequest{}, err - } - queriesByPanel := models.GetQueriesFromDashboard(dashboard.Data) if _, ok := queriesByPanel[panelId]; !ok { return dtos.MetricRequest{}, models.ErrPublicDashboardPanelNotFound } + ts := publicDashboard.BuildTimeSettings(dashboard) + return dtos.MetricRequest{ - From: timeSettings.From, - To: timeSettings.To, + From: ts.From, + To: ts.To, Queries: queriesByPanel[panelId], }, nil } + +// generates a uuid formatted without dashes to use as access token +func GenerateAccessToken() (string, error) { + token, err := uuid.NewV4() + if err != nil { + return "", err + } + + return fmt.Sprintf("%x", token), nil +} diff --git a/pkg/services/dashboards/service/dashboard_public_test.go b/pkg/services/dashboards/service/dashboard_public_test.go index 28d66feed9f..758d8ee8371 100644 --- a/pkg/services/dashboards/service/dashboard_public_test.go +++ b/pkg/services/dashboards/service/dashboard_public_test.go @@ -3,7 +3,9 @@ package service import ( "context" "testing" + "time" + "github.com/gofrs/uuid" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -15,6 +17,11 @@ import ( "github.com/stretchr/testify/require" ) +var timeSettings, _ = simplejson.NewJson([]byte(`{"from": "now-12", "to": "now"}`)) +var defaultPubdashTimeSettings, _ = simplejson.NewJson([]byte(`{}`)) +var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}}) +var mergedDashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-12", "to": "now"}}) + func TestGetPublicDashboard(t *testing.T) { type storeResp struct { pd *models.PublicDashboard @@ -23,78 +30,90 @@ func TestGetPublicDashboard(t *testing.T) { } testCases := []struct { - name string - uid string - storeResp *storeResp - errResp error - dashResp *models.Dashboard + Name string + AccessToken string + StoreResp *storeResp + ErrResp error + DashResp *models.Dashboard }{ { - name: "returns a dashboard", - uid: "abc123", - storeResp: &storeResp{pd: &models.PublicDashboard{}, d: &models.Dashboard{IsPublic: true}, err: nil}, - errResp: nil, - dashResp: &models.Dashboard{IsPublic: true}, + Name: "returns a dashboard", + AccessToken: "abc123", + StoreResp: &storeResp{ + pd: &models.PublicDashboard{IsEnabled: true}, + d: &models.Dashboard{Uid: "mydashboard", Data: dashboardData}, + err: nil, + }, + ErrResp: nil, + DashResp: &models.Dashboard{Uid: "mydashboard", Data: dashboardData}, }, { - name: "puts pubdash time settings into dashboard", - uid: "abc123", - storeResp: &storeResp{ - pd: &models.PublicDashboard{TimeSettings: `{"from": "now-8", "to": "now"}`}, - d: &models.Dashboard{ - IsPublic: true, - Data: simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "abc", "to": "123"}}), - }, - err: nil}, - errResp: nil, - dashResp: &models.Dashboard{IsPublic: true, Data: simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}})}, + Name: "puts pubdash time settings into dashboard", + AccessToken: "abc123", + StoreResp: &storeResp{ + pd: &models.PublicDashboard{IsEnabled: true, TimeSettings: timeSettings}, + d: &models.Dashboard{Data: dashboardData}, + err: nil, + }, + ErrResp: nil, + DashResp: &models.Dashboard{Data: mergedDashboardData}, }, { - name: "returns ErrPublicDashboardNotFound when isPublic is false", - uid: "abc123", - storeResp: &storeResp{pd: &models.PublicDashboard{}, d: &models.Dashboard{IsPublic: false}, err: nil}, - errResp: models.ErrPublicDashboardNotFound, - dashResp: nil, + Name: "returns ErrPublicDashboardNotFound when isEnabled is false", + AccessToken: "abc123", + StoreResp: &storeResp{ + pd: &models.PublicDashboard{IsEnabled: false}, + d: &models.Dashboard{Uid: "mydashboard"}, + err: nil, + }, + ErrResp: models.ErrPublicDashboardNotFound, + DashResp: nil, }, { - name: "returns ErrPublicDashboardNotFound if PublicDashboard missing", - uid: "abc123", - storeResp: &storeResp{pd: nil, d: nil, err: nil}, - errResp: models.ErrPublicDashboardNotFound, - dashResp: nil, + Name: "returns ErrPublicDashboardNotFound if PublicDashboard missing", + AccessToken: "abc123", + StoreResp: &storeResp{pd: nil, d: nil, err: nil}, + ErrResp: models.ErrPublicDashboardNotFound, + DashResp: nil, }, { - name: "returns ErrPublicDashboardNotFound if Dashboard missing", - uid: "abc123", - storeResp: &storeResp{pd: nil, d: nil, err: nil}, - errResp: models.ErrPublicDashboardNotFound, - dashResp: nil, + Name: "returns ErrPublicDashboardNotFound if Dashboard missing", + AccessToken: "abc123", + StoreResp: &storeResp{pd: nil, d: nil, err: nil}, + ErrResp: models.ErrPublicDashboardNotFound, + DashResp: nil, }, } for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { fakeStore := dashboards.FakeDashboardStore{} service := &DashboardServiceImpl{ log: log.New("test.logger"), dashboardStore: &fakeStore, } - fakeStore.On("GetPublicDashboard", mock.Anything). - Return(test.storeResp.pd, test.storeResp.d, test.storeResp.err) - dashboard, err := service.GetPublicDashboard(context.Background(), test.uid) - if test.errResp != nil { - assert.Error(t, test.errResp, err) + fakeStore.On("GetPublicDashboard", mock.Anything, mock.Anything). + Return(test.StoreResp.pd, test.StoreResp.d, test.StoreResp.err) + + dashboard, err := service.GetPublicDashboard(context.Background(), test.AccessToken) + if test.ErrResp != nil { + assert.Error(t, test.ErrResp, err) } else { require.NoError(t, err) } - assert.Equal(t, test.dashResp, dashboard) + + assert.Equal(t, test.DashResp, dashboard) + + if test.DashResp != nil { + assert.NotNil(t, dashboard.CreatedBy) + } }) } } func TestSavePublicDashboard(t *testing.T) { - t.Run("gets PublicDashboard.orgId and PublicDashboard.DashboardUid set from SavePublicDashboardConfigDTO", func(t *testing.T) { + t.Run("Saving public dashboard", func(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := database.ProvideDashboardStore(sqlStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) @@ -107,56 +126,197 @@ func TestSavePublicDashboard(t *testing.T) { dto := &dashboards.SavePublicDashboardConfigDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, - PublicDashboardConfig: &models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - DashboardUid: "NOTTHESAME", - OrgId: 9999999, - }, + UserId: 7, + PublicDashboard: &models.PublicDashboard{ + IsEnabled: true, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + TimeSettings: timeSettings, }, } - pdc, err := service.SavePublicDashboardConfig(context.Background(), dto) + _, err := service.SavePublicDashboardConfig(context.Background(), dto) require.NoError(t, err) - assert.Equal(t, dashboard.Uid, pdc.PublicDashboard.DashboardUid) - assert.Equal(t, dashboard.OrgId, pdc.PublicDashboard.OrgId) + pubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid) + require.NoError(t, err) + + // DashboardUid/OrgId/CreatedBy set by the command, not parameters + assert.Equal(t, dashboard.Uid, pubdash.DashboardUid) + assert.Equal(t, dashboard.OrgId, pubdash.OrgId) + assert.Equal(t, dto.UserId, pubdash.CreatedBy) + // IsEnabled set by parameters + assert.Equal(t, dto.PublicDashboard.IsEnabled, pubdash.IsEnabled) + // CreatedAt set to non-zero time + assert.NotEqual(t, &time.Time{}, pubdash.CreatedAt) + // Time settings set by db + assert.Equal(t, timeSettings, pubdash.TimeSettings) + // accessToken is valid uuid + _, err = uuid.FromString(pubdash.AccessToken) + require.NoError(t, err) }) - t.Run("PLACEHOLDER - dashboard with template variables cannot be saved", func(t *testing.T) { - //sqlStore := sqlstore.InitTestDB(t) - //dashboardStore := database.ProvideDashboardStore(sqlStore) - //dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + t.Run("Validate pubdash has default time setting value", func(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - //service := &DashboardServiceImpl{ - //log: log.New("test.logger"), - //dashboardStore: dashboardStore, - //} + service := &DashboardServiceImpl{ + log: log.New("test.logger"), + dashboardStore: dashboardStore, + } - //dto := &dashboards.SavePublicDashboardConfigDTO{ - //DashboardUid: dashboard.Uid, - //OrgId: dashboard.OrgId, - //PublicDashboardConfig: &models.PublicDashboardConfig{ - //IsPublic: true, - //PublicDashboard: models.PublicDashboard{ - //DashboardUid: "NOTTHESAME", - //OrgId: 9999999, - //}, - //}, - //} + dto := &dashboards.SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + UserId: 7, + PublicDashboard: &models.PublicDashboard{ + IsEnabled: true, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + }, + } - //pdc, err := service.SavePublicDashboardConfig(context.Background(), dto) - //require.NoError(t, err) + _, err := service.SavePublicDashboardConfig(context.Background(), dto) + require.NoError(t, err) - //assert.Equal(t, dashboard.Uid, pdc.PublicDashboard.DashboardUid) - //assert.Equal(t, dashboard.OrgId, pdc.PublicDashboard.OrgId) + pubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid) + require.NoError(t, err) + assert.Equal(t, defaultPubdashTimeSettings, pubdash.TimeSettings) + }) + + t.Run("PLACEHOLDER - dashboard with template variables cannot be saved", func(t *testing.T) {}) +} + +func TestUpdatePublicDashboard(t *testing.T) { + t.Run("Updating public dashboard", func(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + + service := &DashboardServiceImpl{ + log: log.New("test.logger"), + dashboardStore: dashboardStore, + } + + dto := &dashboards.SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + UserId: 7, + PublicDashboard: &models.PublicDashboard{ + IsEnabled: true, + TimeSettings: timeSettings, + }, + } + + _, err := service.SavePublicDashboardConfig(context.Background(), dto) + require.NoError(t, err) + + savedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid) + require.NoError(t, err) + + // attempt to overwrite settings + dto = &dashboards.SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + UserId: 8, + PublicDashboard: &models.PublicDashboard{ + Uid: savedPubdash.Uid, + OrgId: 9, + DashboardUid: "abc1234", + CreatedBy: 9, + CreatedAt: time.Time{}, + + IsEnabled: true, + TimeSettings: timeSettings, + AccessToken: "NOTAREALUUID", + }, + } + + // Since the dto.PublicDashboard has a uid, this will call + // service.updatePublicDashboardConfig + _, err = service.SavePublicDashboardConfig(context.Background(), dto) + require.NoError(t, err) + + updatedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid) + require.NoError(t, err) + + // don't get updated + assert.Equal(t, savedPubdash.DashboardUid, updatedPubdash.DashboardUid) + assert.Equal(t, savedPubdash.OrgId, updatedPubdash.OrgId) + assert.Equal(t, savedPubdash.CreatedAt, updatedPubdash.CreatedAt) + assert.Equal(t, savedPubdash.CreatedBy, updatedPubdash.CreatedBy) + assert.Equal(t, savedPubdash.AccessToken, updatedPubdash.AccessToken) + + // gets updated + assert.Equal(t, dto.PublicDashboard.IsEnabled, updatedPubdash.IsEnabled) + assert.Equal(t, dto.PublicDashboard.TimeSettings, updatedPubdash.TimeSettings) + assert.Equal(t, dto.UserId, updatedPubdash.UpdatedBy) + assert.NotEqual(t, &time.Time{}, updatedPubdash.UpdatedAt) + }) + + t.Run("Updating set empty time settings", func(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + + service := &DashboardServiceImpl{ + log: log.New("test.logger"), + dashboardStore: dashboardStore, + } + + dto := &dashboards.SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + UserId: 7, + PublicDashboard: &models.PublicDashboard{ + IsEnabled: true, + TimeSettings: timeSettings, + }, + } + + // Since the dto.PublicDashboard has a uid, this will call + // service.updatePublicDashboardConfig + _, err := service.SavePublicDashboardConfig(context.Background(), dto) + require.NoError(t, err) + + savedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid) + require.NoError(t, err) + + // attempt to overwrite settings + dto = &dashboards.SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + UserId: 8, + PublicDashboard: &models.PublicDashboard{ + Uid: savedPubdash.Uid, + OrgId: 9, + DashboardUid: "abc1234", + CreatedBy: 9, + CreatedAt: time.Time{}, + + IsEnabled: true, + AccessToken: "NOTAREALUUID", + }, + } + + _, err = service.SavePublicDashboardConfig(context.Background(), dto) + require.NoError(t, err) + + updatedPubdash, err := service.GetPublicDashboardConfig(context.Background(), dashboard.OrgId, dashboard.Uid) + require.NoError(t, err) + + timeSettings, err := simplejson.NewJson([]byte("{}")) + require.NoError(t, err) + + assert.Equal(t, timeSettings, updatedPubdash.TimeSettings) }) } func TestBuildPublicDashboardMetricRequest(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := database.ProvideDashboardStore(sqlStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true) service := &DashboardServiceImpl{ @@ -165,47 +325,44 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) { } dto := &dashboards.SavePublicDashboardConfigDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, - PublicDashboardConfig: &models.PublicDashboardConfig{ - IsPublic: true, - PublicDashboard: models.PublicDashboard{ - DashboardUid: "NOTTHESAME", - OrgId: 9999999, - TimeSettings: `{"from": "FROM", "to": "TO"}`, - }, + DashboardUid: publicDashboard.Uid, + OrgId: publicDashboard.OrgId, + PublicDashboard: &models.PublicDashboard{ + IsEnabled: true, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + TimeSettings: timeSettings, }, } - pdc, err := service.SavePublicDashboardConfig(context.Background(), dto) + publicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), dto) require.NoError(t, err) nonPublicDto := &dashboards.SavePublicDashboardConfigDTO{ DashboardUid: nonPublicDashboard.Uid, OrgId: nonPublicDashboard.OrgId, - PublicDashboardConfig: &models.PublicDashboardConfig{ - IsPublic: false, - PublicDashboard: models.PublicDashboard{ - DashboardUid: "NOTTHESAME", - OrgId: 9999999, - TimeSettings: `{"from": "FROM", "to": "TO"}`, - }, + PublicDashboard: &models.PublicDashboard{ + IsEnabled: false, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + TimeSettings: defaultPubdashTimeSettings, }, } - nonPublicPdc, err := service.SavePublicDashboardConfig(context.Background(), nonPublicDto) + nonPublicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), nonPublicDto) require.NoError(t, err) t.Run("extracts queries from provided dashboard", func(t *testing.T) { reqDTO, err := service.BuildPublicDashboardMetricRequest( context.Background(), - pdc.PublicDashboard.Uid, + publicDashboard, + publicDashboardPD, 1, ) require.NoError(t, err) - require.Equal(t, "FROM", reqDTO.From) - require.Equal(t, "TO", reqDTO.To) + require.Equal(t, timeSettings.Get("from").MustString(), reqDTO.From) + require.Equal(t, timeSettings.Get("to").MustString(), reqDTO.To) require.Len(t, reqDTO.Queries, 2) require.Equal( t, @@ -234,7 +391,8 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) { t.Run("returns an error when panel missing", func(t *testing.T) { _, err := service.BuildPublicDashboardMetricRequest( context.Background(), - pdc.PublicDashboard.Uid, + publicDashboard, + publicDashboardPD, 49, ) @@ -244,7 +402,8 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) { t.Run("returns an error when dashboard not public", func(t *testing.T) { _, err := service.BuildPublicDashboardMetricRequest( context.Background(), - nonPublicPdc.PublicDashboard.Uid, + nonPublicDashboard, + nonPublicDashboardPD, 2, ) require.ErrorContains(t, err, "Public dashboard not found") @@ -262,19 +421,19 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, "id": nil, "title": title, "tags": tags, - "panels": []map[string]interface{}{ - { + "panels": []interface{}{ + map[string]interface{}{ "id": 1, - "targets": []map[string]interface{}{ - { - "datasource": map[string]string{ + "targets": []interface{}{ + map[string]interface{}{ + "datasource": map[string]interface{}{ "type": "mysql", "uid": "ds1", }, "refId": "A", }, - { - "datasource": map[string]string{ + map[string]interface{}{ + "datasource": map[string]interface{}{ "type": "prometheus", "uid": "ds2", }, @@ -282,11 +441,11 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, }, }, }, - { + map[string]interface{}{ "id": 2, - "targets": []map[string]interface{}{ - { - "datasource": map[string]string{ + "targets": []interface{}{ + map[string]interface{}{ + "datasource": map[string]interface{}{ "type": "mysql", "uid": "ds3", }, diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index d085bfce404..20ce1c3e75d 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -67,6 +67,27 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models. return r0, r1 } +// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx +func (_m *FakeDashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDashboard provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) { ret := _m.Called(ctx, query) @@ -298,13 +319,13 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dash return r0, r1 } -// GetPublicDashboard provides a mock function with given fields: uid -func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) { - ret := _m.Called(uid) +// GetPublicDashboard provides a mock function with given fields: ctx, accessToken +func (_m *FakeDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) { + ret := _m.Called(ctx, accessToken) var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(string) *models.PublicDashboard); ok { - r0 = rf(uid) + if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { + r0 = rf(ctx, accessToken) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.PublicDashboard) @@ -312,8 +333,8 @@ func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDash } var r1 *models.Dashboard - if rf, ok := ret.Get(1).(func(string) *models.Dashboard); ok { - r1 = rf(uid) + if rf, ok := ret.Get(1).(func(context.Context, string) *models.Dashboard); ok { + r1 = rf(ctx, accessToken) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*models.Dashboard) @@ -321,8 +342,8 @@ func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDash } var r2 error - if rf, ok := ret.Get(2).(func(string) error); ok { - r2 = rf(uid) + if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { + r2 = rf(ctx, accessToken) } else { r2 = ret.Error(2) } @@ -330,22 +351,22 @@ func (_m *FakeDashboardStore) GetPublicDashboard(uid string) (*models.PublicDash return r0, r1, r2 } -// GetPublicDashboardConfig provides a mock function with given fields: orgId, dashboardUid -func (_m *FakeDashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) { - ret := _m.Called(orgId, dashboardUid) +// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid +func (_m *FakeDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { + ret := _m.Called(ctx, orgId, dashboardUid) - var r0 *models.PublicDashboardConfig - if rf, ok := ret.Get(0).(func(int64, string) *models.PublicDashboardConfig); ok { - r0 = rf(orgId, dashboardUid) + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok { + r0 = rf(ctx, orgId, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboardConfig) + r0 = ret.Get(0).(*models.PublicDashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(int64, string) error); ok { - r1 = rf(orgId, dashboardUid) + if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { + r1 = rf(ctx, orgId, dashboardUid) } else { r1 = ret.Error(1) } @@ -441,22 +462,22 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardC return r0, r1 } -// SavePublicDashboardConfig provides a mock function with given fields: cmd -func (_m *FakeDashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) { - ret := _m.Called(cmd) +// SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd +func (_m *FakeDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) { + ret := _m.Called(ctx, cmd) - var r0 *models.PublicDashboardConfig - if rf, ok := ret.Get(0).(func(models.SavePublicDashboardConfigCommand) *models.PublicDashboardConfig); ok { - r0 = rf(cmd) + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) *models.PublicDashboard); ok { + r0 = rf(ctx, cmd) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboardConfig) + r0 = ret.Get(0).(*models.PublicDashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(models.SavePublicDashboardConfigCommand) error); ok { - r1 = rf(cmd) + if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { + r1 = rf(ctx, cmd) } else { r1 = ret.Error(1) } @@ -492,6 +513,20 @@ func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64, return r0 } +// UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd +func (_m *FakeDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error { + ret := _m.Called(ctx, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ValidateDashboardBeforeSave provides a mock function with given fields: dashboard, overwrite func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { ret := _m.Called(dashboard, overwrite) diff --git a/pkg/services/live/telemetry/telegraf/testdata/changing_types_NaN.jsonc b/pkg/services/live/telemetry/telegraf/testdata/changing_types_NaN.jsonc index bbe3b08294e..687e5bcd134 100644 --- a/pkg/services/live/telemetry/telegraf/testdata/changing_types_NaN.jsonc +++ b/pkg/services/live/telemetry/telegraf/testdata/changing_types_NaN.jsonc @@ -98,4 +98,4 @@ } } ] -} \ No newline at end of file +} diff --git a/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go b/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go deleted file mode 100644 index a53c80e6b6d..00000000000 --- a/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go +++ /dev/null @@ -1,33 +0,0 @@ -package migrations - -import ( - . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" -) - -func addPublicDashboardMigration(mg *Migrator) { - var dashboardPublicCfgV1 = Table{ - Name: "dashboard_public_config", - Columns: []*Column{ - {Name: "uid", Type: DB_NVarchar, Length: 40, IsPrimaryKey: true}, - {Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: false}, - {Name: "org_id", Type: DB_BigInt, Nullable: false}, - {Name: "time_settings", Type: DB_Text, Nullable: false}, - {Name: "refresh_rate", Type: DB_Int, Nullable: false, Default: "30"}, - {Name: "template_variables", Type: DB_MediumText, Nullable: true}, - }, - Indices: []*Index{ - {Cols: []string{"uid"}, Type: UniqueIndex}, - {Cols: []string{"org_id", "dashboard_uid"}}, - }, - } - mg.AddMigration("create dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1)) - - // table has no dependencies and was created with incorrect pkey type. - // drop then recreate with correct values - addDropAllIndicesMigrations(mg, "v1", dashboardPublicCfgV1) - mg.AddMigration("Drop old dashboard public config table", NewDropTableMigration("dashboard_public_config")) - - // recreate table with proper primary key type - mg.AddMigration("recreate dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1)) - addTableIndicesMigrations(mg, "v1", dashboardPublicCfgV1) -} diff --git a/pkg/services/sqlstore/migrations/dashboard_public_mig.go b/pkg/services/sqlstore/migrations/dashboard_public_mig.go new file mode 100644 index 00000000000..22c8a42a4d7 --- /dev/null +++ b/pkg/services/sqlstore/migrations/dashboard_public_mig.go @@ -0,0 +1,52 @@ +package migrations + +import ( + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +func addPublicDashboardMigration(mg *Migrator) { + var dashboardPublicCfgV1 = Table{ + Name: "dashboard_public_config", + Columns: []*Column{ + {Name: "uid", Type: DB_NVarchar, Length: 40, IsPrimaryKey: true}, + {Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: false}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + + {Name: "time_settings", Type: DB_Text, Nullable: true}, + {Name: "template_variables", Type: DB_MediumText, Nullable: true}, + + {Name: "access_token", Type: DB_NVarchar, Length: 32, Nullable: false}, + + {Name: "created_by", Type: DB_Int, Nullable: false}, + {Name: "updated_by", Type: DB_Int, Nullable: true}, + + {Name: "created_at", Type: DB_DateTime, Nullable: false}, + {Name: "updated_at", Type: DB_DateTime, Nullable: true}, + + {Name: "is_enabled", Type: DB_Bool, Nullable: false, Default: "0"}, + }, + Indices: []*Index{ + {Cols: []string{"uid"}, Type: UniqueIndex}, + {Cols: []string{"org_id", "dashboard_uid"}}, + {Cols: []string{"access_token"}, Type: UniqueIndex}, + }, + } + + // initial create table + mg.AddMigration("create dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1)) + + // recreate table - no dependencies and was created with incorrect pkey type + addDropAllIndicesMigrations(mg, "v1", dashboardPublicCfgV1) + mg.AddMigration("Drop old dashboard public config table", NewDropTableMigration("dashboard_public_config")) + mg.AddMigration("recreate dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1)) + addTableIndicesMigrations(mg, "v1", dashboardPublicCfgV1) + + // recreate table - schema finalized for public dashboards v1 + addDropAllIndicesMigrations(mg, "v2", dashboardPublicCfgV1) + mg.AddMigration("Drop public config table", NewDropTableMigration("dashboard_public_config")) + mg.AddMigration("Recreate dashboard public config v2", NewAddTableMigration(dashboardPublicCfgV1)) + addTableIndicesMigrations(mg, "v2", dashboardPublicCfgV1) + + // rename table + addTableRenameMigration(mg, "dashboard_public_config", "dashboard_public", "v2") +} diff --git a/public/api-merged.json b/public/api-merged.json index 8e606dad2ab..f3860817416 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -10423,7 +10423,7 @@ "provisionedExternalId": { "type": "string" }, - "publicDashboardUid": { + "publicDashboardAccessToken": { "type": "string" }, "slug": { diff --git a/public/api-spec.json b/public/api-spec.json index 75df54dec92..e5b0b52cb6f 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -9465,7 +9465,7 @@ "provisionedExternalId": { "type": "string" }, - "publicDashboardUid": { + "publicDashboardAccessToken": { "type": "string" }, "slug": { diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx index dd460df3d24..b9bb5a4cc15 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx @@ -6,12 +6,14 @@ import config from 'app/core/config'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { ShareModal } from './ShareModal'; -import { PublicDashboardConfig } from './SharePublicDashboardUtils'; +import { PublicDashboard } from './SharePublicDashboardUtils'; // Mock api request -const publicDashboardconfigResp: PublicDashboardConfig = { - isPublic: true, - publicDashboard: { uid: '', dashboardUid: '' }, +const publicDashboardconfigResp: PublicDashboard = { + isEnabled: true, + uid: '', + dashboardUid: '', + accessToken: '', }; const backendSrv = { @@ -88,6 +90,13 @@ describe('SharePublic', () => { fireEvent.click(screen.getByText('Public Dashboard')); - await waitFor(() => screen.getByText('Enabled')); + await screen.findByText('Welcome to Grafana public dashboards alpha!'); }); + + // test when checkboxes show up + // test checkboxes hidden + // test url hidden + // test url shows up + // + // test checking if current version of dashboard in state is persisted to db }); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx index 6363dce4035..b00f433c64f 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx @@ -1,34 +1,46 @@ -import React, { useState, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; -import { Button, Field, Switch, Alert } from '@grafana/ui'; +import { AppEvents } from '@grafana/data'; +import { Alert, Button, Checkbox, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; -import { VariableModel } from 'app/features/variables/types'; +import { appEvents } from 'app/core/core'; import { dispatch } from 'app/store/store'; import { dashboardHasTemplateVariables, + generatePublicDashboardUrl, getPublicDashboardConfig, + PublicDashboard, + publicDashboardPersisted, savePublicDashboardConfig, - PublicDashboardConfig, } from './SharePublicDashboardUtils'; import { ShareModalTabProps } from './types'; interface Props extends ShareModalTabProps {} +interface Acknowledgements { + public: boolean; + datasources: boolean; + usage: boolean; +} + export const SharePublicDashboard = (props: Props) => { - const dashboardUid = props.dashboard.uid; - const [publicDashboardConfig, setPublicDashboardConfig] = useState({ - isPublic: false, - publicDashboard: { uid: '', dashboardUid }, + const dashboardVariables = props.dashboard.getVariables(); + const [publicDashboard, setPublicDashboardConfig] = useState({ + isEnabled: false, + uid: '', + dashboardUid: props.dashboard.uid, + }); + const [acknowledgements, setAcknowledgements] = useState({ + public: false, + datasources: false, + usage: false, }); - const [dashboardVariables, setDashboardVariables] = useState([]); - useEffect(() => { - setDashboardVariables(props.dashboard.getVariables()); - getPublicDashboardConfig(dashboardUid, setPublicDashboardConfig).catch(); - }, [props, dashboardUid]); + getPublicDashboardConfig(props.dashboard.uid, setPublicDashboardConfig).catch(); + }, [props.dashboard.uid]); const onSavePublicConfig = () => { if (dashboardHasTemplateVariables(dashboardVariables)) { @@ -38,32 +50,143 @@ export const SharePublicDashboard = (props: Props) => { return; } - savePublicDashboardConfig(props.dashboard.uid, publicDashboardConfig, setPublicDashboardConfig).catch(); + savePublicDashboardConfig(props.dashboard.uid, publicDashboard, setPublicDashboardConfig).catch(); + }; + + const onShareUrlCopy = () => { + appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']); + }; + + const onAcknowledge = useCallback( + (field: string, checked: boolean) => { + setAcknowledgements({ ...acknowledgements, [field]: checked }); + }, + [acknowledgements] + ); + + // check if all conditions have been acknowledged + const acknowledged = () => { + return acknowledgements.public && acknowledgements.datasources && acknowledgements.usage; }; return ( <> - {dashboardHasTemplateVariables(dashboardVariables) && ( +

Welcome to Grafana public dashboards alpha!

+ {dashboardHasTemplateVariables(dashboardVariables) ? ( This dashboard cannot be made public because it has template variables + ) : ( + <> +

+ To allow the current dashboard to be published publicly, toggle the switch. For now we do not support + template variables or frontend datasources. +

+ We'd love your feedback. To share, please comment on this{' '} + + github discussion + +
+ {!publicDashboardPersisted(publicDashboard) && ( +
+ Before you click Save, please acknowledge the following information:
+
+
+
+ onAcknowledge('public', e.currentTarget.checked)} + /> +
+
+
+ onAcknowledge('datasources', e.currentTarget.checked)} + /> +
+
+ onAcknowledge('usage', e.currentTarget.checked)} + /> +
+
+
+
+ )} + {(publicDashboardPersisted(publicDashboard) || acknowledged()) && ( +
+

Public Dashboard Configuration

+
+ Time Range +
+
+ + From: + + } + /> + To: + } + /> +
+
+ + + setPublicDashboardConfig({ + ...publicDashboard, + isEnabled: !publicDashboard.isEnabled, + }) + } + /> + + {publicDashboardPersisted(publicDashboard) && publicDashboard.isEnabled && ( + + { + return generatePublicDashboardUrl(publicDashboard); + }} + onClipboardCopy={onShareUrlCopy} + > + Copy + + } + /> + + )} +
+ +
+ )} + )} - -

Public Dashboard Configuration

- - - - setPublicDashboardConfig((state) => { - return { ...state, isPublic: !state.isPublic }; - }) - } - /> - - ); }; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx index a42188b122d..b04197b196d 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx @@ -1,6 +1,11 @@ import { VariableModel } from 'app/features/variables/types'; -import { dashboardHasTemplateVariables } from './SharePublicDashboardUtils'; +import { + PublicDashboard, + dashboardHasTemplateVariables, + generatePublicDashboardUrl, + publicDashboardPersisted, +} from './SharePublicDashboardUtils'; describe('dashboardHasTemplateVariables', () => { it('false', () => { @@ -14,3 +19,24 @@ describe('dashboardHasTemplateVariables', () => { expect(dashboardHasTemplateVariables(variables)).toBe(true); }); }); + +describe('generatePublicDashboardUrl', () => { + it('has the right uid', () => { + let pubdash = { accessToken: 'abcd1234' } as PublicDashboard; + expect(generatePublicDashboardUrl(pubdash)).toEqual(`${window.location.origin}/public-dashboards/abcd1234`); + }); +}); + +describe('publicDashboardPersisted', () => { + it('true', () => { + let pubdash = { uid: 'abcd1234' } as PublicDashboard; + expect(publicDashboardPersisted(pubdash)).toBe(true); + }); + + it('false', () => { + let pubdash = { uid: '' } as PublicDashboard; + expect(publicDashboardPersisted(pubdash)).toBe(false); + pubdash = {} as PublicDashboard; + expect(publicDashboardPersisted(pubdash)).toBe(false); + }); +}); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts index bd6a0dc60eb..9158d726c63 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts @@ -5,39 +5,53 @@ import { VariableModel } from 'app/features/variables/types'; import { dispatch } from 'app/store/store'; import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard'; -export interface PublicDashboardConfig { - isPublic: boolean; - publicDashboard: { - uid: string; - dashboardUid: string; - timeSettings?: object; - }; +export interface PublicDashboard { + accessToken?: string; + isEnabled: boolean; + uid: string; + dashboardUid: string; + timeSettings?: object; } + export interface DashboardResponse { dashboard: DashboardDataDTO; meta: DashboardMeta; } -export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => { - return variables.length > 0; -}; - export const getPublicDashboardConfig = async ( dashboardUid: string, - setPublicDashboardConfig: React.Dispatch> + setPublicDashboard: React.Dispatch> ) => { const url = `/api/dashboards/uid/${dashboardUid}/public-config`; - const pdResp: PublicDashboardConfig = await getBackendSrv().get(url); - setPublicDashboardConfig(pdResp); + const pdResp: PublicDashboard = await getBackendSrv().get(url); + setPublicDashboard(pdResp); }; export const savePublicDashboardConfig = async ( dashboardUid: string, - publicDashboardConfig: PublicDashboardConfig, - setPublicDashboardConfig: Function + publicDashboardConfig: PublicDashboard, + setPublicDashboard: React.Dispatch> ) => { const url = `/api/dashboards/uid/${dashboardUid}/public-config`; - const pdResp: PublicDashboardConfig = await getBackendSrv().post(url, publicDashboardConfig); + const pdResp: PublicDashboard = await getBackendSrv().post(url, publicDashboardConfig); + + // Never allow a user to send the orgId + // @ts-ignore + delete pdResp.orgId; + dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved'))); - setPublicDashboardConfig(pdResp); + setPublicDashboard(pdResp); +}; + +// Instance methods +export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => { + return variables.length > 0; +}; + +export const publicDashboardPersisted = (publicDashboard: PublicDashboard): boolean => { + return publicDashboard.uid !== '' && publicDashboard.uid !== undefined; +}; + +export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => { + return `${window.location.origin}/public-dashboards/${publicDashboard.accessToken}`; }; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 1300edba06b..270f624efe9 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -37,6 +37,7 @@ export interface DashboardPageRouteParams { uid?: string; type?: string; slug?: string; + accessToken?: string; } export type DashboardPageRouteSearchParams = { @@ -130,6 +131,7 @@ export class UnthemedDashboardPage extends PureComponent { urlFolderId: queryParams.folderId, routeName: this.props.route.routeName, fixUrl: !isPublic, + accessToken: match.params.accessToken, }); // small delay to start live updates diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 31b06afe7cb..81163eac6e4 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -361,7 +361,7 @@ export class PanelChrome extends PureComponent { dashboard.getTimezone(), timeData, width, - dashboard.meta.publicDashboardUid + dashboard.meta.publicDashboardAccessToken ); } else { // The panel should render on refresh as well if it doesn't have a query, like clock panel diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.test.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.test.tsx index d62b7b908c4..f6376c2e2eb 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.test.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.test.tsx @@ -16,7 +16,7 @@ let panelModel = new PanelModel({ let panelData = createEmptyQueryResponse(); describe('Panel Header', () => { - const dashboardModel = new DashboardModel({}, { isPublic: true }); + const dashboardModel = new DashboardModel({}, { publicDashboardAccessToken: 'abc123' }); it('will render header title but not render dropdown icon when dashboard is being viewed publicly', () => { window.history.pushState({}, 'Test Title', '/public-dashboards/abc123'); @@ -29,7 +29,7 @@ describe('Panel Header', () => { }); it('will render header title and dropdown icon when dashboard is not being viewed publicly', () => { - const dashboardModel = new DashboardModel({}, { isPublic: false }); + const dashboardModel = new DashboardModel({}, { publicDashboardAccessToken: '' }); window.history.pushState({}, 'Test Title', '/d/abc/123'); render( diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 3946d551383..7d8eff8de7d 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -59,7 +59,7 @@ export const PanelHeader: FC = ({ panel, error, isViewing, isEditing, dat /> ) : null}

{title}

- {!dashboard.meta.isPublic && ( + {!dashboard.meta.publicDashboardAccessToken && (
{ if (config.featureToggles.publicDashboards) { return [ { - path: '/public-dashboards/:uid', + path: '/public-dashboards/:accessToken', pageClass: 'page-dashboard', routeName: DashboardRoutes.Public, component: SafeDynamicImport( diff --git a/public/app/features/dashboard/services/PublicDashboardDataSource.ts b/public/app/features/dashboard/services/PublicDashboardDataSource.ts index 4d75ebe7906..7a384b0835d 100644 --- a/public/app/features/dashboard/services/PublicDashboardDataSource.ts +++ b/public/app/features/dashboard/services/PublicDashboardDataSource.ts @@ -20,13 +20,13 @@ export class PublicDashboardDataSource extends DataSourceApi { * Ideally final -- any other implementation may not work as expected */ query(request: DataQueryRequest): Observable { - const { intervalMs, maxDataPoints, range, requestId, publicDashboardUid, panelId } = request; + const { intervalMs, maxDataPoints, range, requestId, publicDashboardAccessToken, panelId } = request; let targets = request.targets; const queries = targets.map((q) => { return { ...q, - publicDashboardUid, + publicDashboardAccessToken, intervalMs, maxDataPoints, }; @@ -37,7 +37,7 @@ export class PublicDashboardDataSource extends DataSourceApi { return of({ data: [] }); } - const body: any = { queries, publicDashboardUid, panelId }; + const body: any = { queries, publicDashboardAccessToken, panelId }; if (range) { body.range = range; @@ -47,7 +47,7 @@ export class PublicDashboardDataSource extends DataSourceApi { return getBackendSrv() .fetch({ - url: `/api/public/dashboards/${publicDashboardUid}/panels/${panelId}/query`, + url: `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query`, method: 'POST', data: body, requestId, diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index bf5d098184c..cca68f6b76b 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -319,14 +319,14 @@ export class PanelModel implements DataConfigSource, IPanelModel { dashboardTimezone: string, timeData: TimeOverrideResult, width: number, - publicDashboardUid?: string + publicDashboardAccessToken?: string ) { this.getQueryRunner().run({ datasource: this.datasource, queries: this.targets, panelId: this.id, dashboardId: dashboardId, - publicDashboardUid, + publicDashboardAccessToken, timezone: dashboardTimezone, timeRange: timeData.timeRange, timeInfo: timeData.timeInfo, diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index ddd7fdc7dc2..027f427956a 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -25,6 +25,7 @@ export interface InitDashboardArgs { urlSlug?: string; urlType?: string; urlFolderId?: string | null; + accessToken?: string; routeName?: string; fixUrl: boolean; } @@ -61,7 +62,7 @@ async function fetchDashboard( return dashDTO; } case DashboardRoutes.Public: { - return await dashboardLoaderSrv.loadDashboard('public', args.urlSlug, args.urlUid); + return await dashboardLoaderSrv.loadDashboard('public', args.urlSlug, args.accessToken); } case DashboardRoutes.Normal: { const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid); diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 4cf9e5e7930..a298229d44e 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -47,7 +47,7 @@ export interface QueryRunnerOptions< queries: TQuery[]; panelId?: number; dashboardId?: number; - publicDashboardUid?: string; + publicDashboardAccessToken?: string; timezone: TimeZone; timeRange: TimeRange; timeInfo?: string; // String description of time range for display @@ -203,7 +203,7 @@ export class PanelQueryRunner { datasource, panelId, dashboardId, - publicDashboardUid, + publicDashboardAccessToken, timeRange, timeInfo, cacheTimeout, @@ -223,7 +223,7 @@ export class PanelQueryRunner { timezone, panelId, dashboardId, - publicDashboardUid, + publicDashboardAccessToken, range: timeRange, timeInfo, interval: '', @@ -239,7 +239,7 @@ export class PanelQueryRunner { (request as any).rangeRaw = timeRange.raw; try { - const ds = await getDataSource(datasource, request.scopedVars, publicDashboardUid); + const ds = await getDataSource(datasource, request.scopedVars, publicDashboardAccessToken); const isMixedDS = ds.meta?.mixed; // Attach the data source to each query @@ -359,9 +359,9 @@ export class PanelQueryRunner { async function getDataSource( datasource: DataSourceRef | string | DataSourceApi | null, scopedVars: ScopedVars, - publicDashboardUid?: string + publicDashboardAccessToken?: string ): Promise { - if (publicDashboardUid) { + if (publicDashboardAccessToken) { return new PublicDashboardDataSource(); } diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index dcc957b0caa..912975c7ec8 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -38,8 +38,7 @@ export interface DashboardMeta { fromFile?: boolean; hasUnsavedFolderChange?: boolean; annotationsPermissions?: AnnotationsPermissions; - isPublic?: boolean; - publicDashboardUid?: string; + publicDashboardAccessToken?: string; } export interface AnnotationActions {