diff --git a/pkg/api/api.go b/pkg/api/api.go index 1161e5389c6..ef5cb1e583b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -392,8 +392,8 @@ func (hs *HTTPServer) registerRoutes() { dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) { if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { - dashUidRoute.Get("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetPublicDashboard)) - dashUidRoute.Post("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.SavePublicDashboard)) + dashUidRoute.Get("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetPublicDashboardConfig)) + dashUidRoute.Post("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.SavePublicDashboardConfig)) } if hs.ThumbService != nil { @@ -608,6 +608,11 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey)) r.Delete("/api/snapshots/:key", reqEditorRole, routing.Wrap(hs.DeleteDashboardSnapshot)) + // Public API + if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { + r.Get("/api/public/dashboards/:uid", routing.Wrap(hs.GetPublicDashboard)) + } + // Frontend logs sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS) r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now), diff --git a/pkg/api/dashboard_public.go b/pkg/api/dashboard_public.go new file mode 100644 index 00000000000..fbb71cc9c52 --- /dev/null +++ b/pkg/api/dashboard_public.go @@ -0,0 +1,61 @@ +package api + +import ( + "errors" + "net/http" + + "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/web" +) + +// gets public dashboard +func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response { + dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":uid"]) + if err != nil { + return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err) + } + return response.JSON(http.StatusOK, dash) +} + +// gets public dashboard configuration for dashboard +func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Response { + pdc, err := hs.dashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"]) + if err != nil { + return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err) + } + return response.JSON(http.StatusOK, pdc) +} + +// 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 { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + + dto := dashboards.SavePublicDashboardConfigDTO{ + OrgId: c.OrgId, + DashboardUid: web.Params(c.Req)[":uid"], + PublicDashboardConfig: pdc, + } + + pdc, 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) +} + +// util to help us unpack a dashboard err or use default http code and message +func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response { + var dashboardErr models.DashboardErr + + if ok := errors.As(err, &dashboardErr); ok { + return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr) + } + + return response.Error(defaultCode, defaultMsg, err) +} diff --git a/pkg/api/dashboard_public_config.go b/pkg/api/dashboard_public_config.go deleted file mode 100644 index 7dc903998d1..00000000000 --- a/pkg/api/dashboard_public_config.go +++ /dev/null @@ -1,53 +0,0 @@ -package api - -import ( - "errors" - "net/http" - - "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/web" -) - -// Sets sharing configuration for dashboard -func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response { - pdc, err := hs.dashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"]) - - if errors.Is(err, models.ErrDashboardNotFound) { - return response.Error(http.StatusNotFound, "dashboard not found", err) - } - - if err != nil { - return response.Error(http.StatusInternalServerError, "error retrieving public dashboard config", err) - } - - return response.JSON(http.StatusOK, pdc) -} - -// Sets sharing configuration for dashboard -func (hs *HTTPServer) SavePublicDashboard(c *models.ReqContext) response.Response { - pdc := &models.PublicDashboardConfig{} - - if err := web.Bind(c.Req, pdc); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) - } - - dto := dashboards.SavePublicDashboardConfigDTO{ - OrgId: c.OrgId, - Uid: web.Params(c.Req)[":uid"], - PublicDashboardConfig: *pdc, - } - - pdc, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) - - if errors.Is(err, models.ErrDashboardNotFound) { - return response.Error(http.StatusNotFound, "dashboard not found", err) - } - - if err != nil { - return response.Error(http.StatusInternalServerError, "error updating public dashboard config", err) - } - - return response.JSON(http.StatusOK, pdc) -} diff --git a/pkg/api/dashboard_public_config_test.go b/pkg/api/dashboard_public_config_test.go deleted file mode 100644 index f487b3ea252..00000000000 --- a/pkg/api/dashboard_public_config_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package api - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/featuremgmt" -) - -func TestApiRetrieveConfig(t *testing.T) { - pdc := &models.PublicDashboardConfig{IsPublic: true} - - testCases := []struct { - name string - dashboardUid string - expectedHttpResponse int - publicDashboardConfigResult *models.PublicDashboardConfig - publicDashboardConfigError error - }{ - { - name: "retrieves public dashboard config when dashboard is found", - dashboardUid: "1", - expectedHttpResponse: http.StatusOK, - publicDashboardConfigResult: pdc, - publicDashboardConfigError: nil, - }, - { - name: "returns 404 when dashboard not found", - dashboardUid: "77777", - expectedHttpResponse: http.StatusNotFound, - publicDashboardConfigResult: nil, - publicDashboardConfigError: models.ErrDashboardNotFound, - }, - { - name: "returns 500 when internal server error", - dashboardUid: "1", - expectedHttpResponse: http.StatusInternalServerError, - publicDashboardConfigResult: nil, - publicDashboardConfigError: errors.New("database broken"), - }, - } - - for _, test := range testCases { - 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) - sc.hs.dashboardService = dashSvc - - setInitCtxSignedInViewer(sc.initCtx) - response := callAPI( - sc.server, - http.MethodGet, - "/api/dashboards/uid/1/public-config", - nil, - t, - ) - - assert.Equal(t, test.expectedHttpResponse, response.Code) - - if test.expectedHttpResponse == http.StatusOK { - var pdcResp models.PublicDashboardConfig - err := json.Unmarshal(response.Body.Bytes(), &pdcResp) - require.NoError(t, err) - assert.Equal(t, test.publicDashboardConfigResult, &pdcResp) - } - }) - } -} - -func TestApiPersistsValue(t *testing.T) { - testCases := []struct { - name string - dashboardUid string - expectedHttpResponse int - saveDashboardError error - }{ - { - name: "returns 200 when update persists", - dashboardUid: "1", - expectedHttpResponse: http.StatusOK, - saveDashboardError: nil, - }, - { - name: "returns 500 when not persisted", - expectedHttpResponse: http.StatusInternalServerError, - saveDashboardError: errors.New("backend failed to save"), - }, - { - name: "returns 404 when dashboard not found", - expectedHttpResponse: http.StatusNotFound, - saveDashboardError: models.ErrDashboardNotFound, - }, - } - - for _, test := range testCases { - 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) - sc.hs.dashboardService = dashSvc - - setInitCtxSignedInViewer(sc.initCtx) - response := callAPI( - sc.server, - http.MethodPost, - "/api/dashboards/uid/1/public-config", - strings.NewReader(`{ "isPublic": true }`), - t, - ) - - assert.Equal(t, test.expectedHttpResponse, response.Code) - - // check the result if it's a 200 - if response.Code == http.StatusOK { - respJSON, _ := simplejson.NewJson(response.Body.Bytes()) - val, _ := respJSON.Get("isPublic").Bool() - assert.Equal(t, true, val) - } - }) - } -} diff --git a/pkg/api/dashboard_public_test.go b/pkg/api/dashboard_public_test.go new file mode 100644 index 00000000000..cbd7d30899c --- /dev/null +++ b/pkg/api/dashboard_public_test.go @@ -0,0 +1,227 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +func TestAPIGetPublicDashboard(t *testing.T) { + t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) { + sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures()) + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). + Return(&models.Dashboard{}, nil).Maybe() + sc.hs.dashboardService = dashSvc + + setInitCtxSignedInViewer(sc.initCtx) + response := callAPI( + sc.server, + http.MethodGet, + "/api/public/dashboards", + nil, + t, + ) + assert.Equal(t, http.StatusNotFound, response.Code) + response = callAPI( + sc.server, + http.MethodGet, + "/api/public/dashboards/asdf", + nil, + t, + ) + assert.Equal(t, http.StatusNotFound, response.Code) + }) + + testCases := []struct { + name string + uid string + expectedHttpResponse int + publicDashboardResult *models.Dashboard + publicDashboardErr error + }{ + { + name: "It gets a public dashboard", + uid: "pubdash-abcd1234", + expectedHttpResponse: http.StatusOK, + publicDashboardResult: &models.Dashboard{ + Uid: "dashboard-abcd1234", + }, + publicDashboardErr: nil, + }, + { + name: "It should return 404 if isPublicDashboard is false", + uid: "pubdash-abcd1234", + expectedHttpResponse: http.StatusNotFound, + publicDashboardResult: nil, + publicDashboardErr: models.ErrPublicDashboardNotFound, + }, + } + + for _, test := range testCases { + 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")). + Return(test.publicDashboardResult, test.publicDashboardErr) + sc.hs.dashboardService = dashSvc + + setInitCtxSignedInViewer(sc.initCtx) + response := callAPI( + sc.server, + http.MethodGet, + fmt.Sprintf("/api/public/dashboards/%v", test.uid), + nil, + t, + ) + + assert.Equal(t, test.expectedHttpResponse, response.Code) + + if test.publicDashboardErr == nil { + var dashResp models.Dashboard + err := json.Unmarshal(response.Body.Bytes(), &dashResp) + require.NoError(t, err) + assert.Equal(t, test.publicDashboardResult.Uid, dashResp.Uid) + } else { + var errResp struct { + Error string `json:"error"` + } + err := json.Unmarshal(response.Body.Bytes(), &errResp) + require.NoError(t, err) + assert.Equal(t, test.publicDashboardErr.Error(), errResp.Error) + } + }) + } +} + +func TestAPIGetPublicDashboardConfig(t *testing.T) { + pdc := &models.PublicDashboardConfig{IsPublic: true} + + testCases := []struct { + name string + dashboardUid string + expectedHttpResponse int + publicDashboardConfigResult *models.PublicDashboardConfig + publicDashboardConfigError error + }{ + { + name: "retrieves public dashboard config when dashboard is found", + dashboardUid: "1", + expectedHttpResponse: http.StatusOK, + publicDashboardConfigResult: pdc, + publicDashboardConfigError: nil, + }, + { + name: "returns 404 when dashboard not found", + dashboardUid: "77777", + expectedHttpResponse: http.StatusNotFound, + publicDashboardConfigResult: nil, + publicDashboardConfigError: models.ErrDashboardNotFound, + }, + { + name: "returns 500 when internal server error", + dashboardUid: "1", + expectedHttpResponse: http.StatusInternalServerError, + publicDashboardConfigResult: nil, + publicDashboardConfigError: errors.New("database broken"), + }, + } + + for _, test := range testCases { + 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) + sc.hs.dashboardService = dashSvc + + setInitCtxSignedInViewer(sc.initCtx) + response := callAPI( + sc.server, + http.MethodGet, + "/api/dashboards/uid/1/public-config", + nil, + t, + ) + + assert.Equal(t, test.expectedHttpResponse, response.Code) + + if response.Code == http.StatusOK { + var pdcResp models.PublicDashboardConfig + err := json.Unmarshal(response.Body.Bytes(), &pdcResp) + require.NoError(t, err) + assert.Equal(t, test.publicDashboardConfigResult, &pdcResp) + } + }) + } +} + +func TestApiSavePublicDashboardConfig(t *testing.T) { + testCases := []struct { + name string + dashboardUid string + publicDashboardConfig *models.PublicDashboardConfig + expectedHttpResponse int + saveDashboardError error + }{ + { + name: "returns 200 when update persists", + dashboardUid: "1", + publicDashboardConfig: &models.PublicDashboardConfig{IsPublic: true}, + expectedHttpResponse: http.StatusOK, + saveDashboardError: nil, + }, + { + name: "returns 500 when not persisted", + expectedHttpResponse: http.StatusInternalServerError, + publicDashboardConfig: &models.PublicDashboardConfig{}, + saveDashboardError: errors.New("backend failed to save"), + }, + { + name: "returns 404 when dashboard not found", + expectedHttpResponse: http.StatusNotFound, + publicDashboardConfig: &models.PublicDashboardConfig{}, + saveDashboardError: models.ErrDashboardNotFound, + }, + } + + for _, test := range testCases { + 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) + sc.hs.dashboardService = dashSvc + + setInitCtxSignedInViewer(sc.initCtx) + response := callAPI( + sc.server, + http.MethodPost, + "/api/dashboards/uid/1/public-config", + strings.NewReader(`{ "isPublic": true }`), + t, + ) + + assert.Equal(t, test.expectedHttpResponse, response.Code) + + // check the result if it's a 200 + if response.Code == http.StatusOK { + val, err := json.Marshal(test.publicDashboardConfig) + require.NoError(t, err) + assert.Equal(t, string(val), response.Body.String()) + } + }) + } +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index af229e6ddaa..1a68389b262 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -206,10 +206,6 @@ type Dashboard struct { Data *simplejson.Json } -type PublicDashboardConfig struct { - IsPublic bool `json:"isPublic"` -} - func (d *Dashboard) SetId(id int64) { d.Id = id d.Data.Set("id", id) @@ -417,12 +413,6 @@ type DeleteOrphanedProvisionedDashboardsCommand struct { ReaderNames []string } -type SavePublicDashboardConfigCommand struct { - Uid string - OrgId int64 - PublicDashboardConfig PublicDashboardConfig -} - // // QUERIES // diff --git a/pkg/models/dashboards_public.go b/pkg/models/dashboards_public.go new file mode 100644 index 00000000000..8804174f809 --- /dev/null +++ b/pkg/models/dashboards_public.go @@ -0,0 +1,43 @@ +package models + +var ( + ErrPublicDashboardFailedGenerateUniqueUid = DashboardErr{ + Reason: "Failed to generate unique dashboard id", + StatusCode: 500, + } + ErrPublicDashboardNotFound = DashboardErr{ + Reason: "Public dashboard not found", + StatusCode: 404, + Status: "not-found", + } + ErrPublicDashboardIdentifierNotSet = DashboardErr{ + Reason: "No Uid for public dashboard specified", + StatusCode: 400, + } +) + +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"` +} + +func (pd PublicDashboard) TableName() string { + return "dashboard_public_config" +} + +// +// COMMANDS +// + +type SavePublicDashboardConfigCommand struct { + DashboardUid string + OrgId int64 + PublicDashboardConfig PublicDashboardConfig +} diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 031e7c39d52..db64ff76d8f 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -17,6 +17,7 @@ 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) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error @@ -63,6 +64,7 @@ type Store interface { 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) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error // SaveAlerts saves dashboard alerts. diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 4e22249eb12..08a0de76b44 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 @@ -146,6 +146,29 @@ 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) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { + r0 = rf(ctx, publicDashboardUid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, publicDashboardUid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // 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) { ret := _m.Called(ctx, orgId, dashboardUid) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index de18a96b1f2..70bba04ed0c 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -192,47 +192,6 @@ func (d *DashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models return cmd.Result, err } -// retrieves public dashboard configuration -func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) { - var result []*models.Dashboard - - err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - return sess.Where("org_id = ? AND uid= ?", orgId, dashboardUid).Find(&result) - }) - - if len(result) == 0 { - return nil, models.ErrDashboardNotFound - } - - pdc := &models.PublicDashboardConfig{ - IsPublic: result[0].IsPublic, - } - - return pdc, err -} - -// stores public dashboard configuration -func (d *DashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboardConfig, error) { - err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - affectedRowCount, err := sess.Table("dashboard").Where("org_id = ? AND uid = ?", cmd.OrgId, cmd.Uid).Update(map[string]interface{}{"is_public": cmd.PublicDashboardConfig.IsPublic}) - if err != nil { - return err - } - - if affectedRowCount == 0 { - return models.ErrDashboardNotFound - } - - return nil - }) - - if err != nil { - return nil, err - } - - return &cmd.PublicDashboardConfig, nil -} - func (d *DashboardStore) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*models.DashboardAcl) error { return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { // delete existing items diff --git a/pkg/services/dashboards/database/database_dashboard_public.go b/pkg/services/dashboards/database/database_dashboard_public.go new file mode 100644 index 00000000000..392d9ce3de1 --- /dev/null +++ b/pkg/services/dashboards/database/database_dashboard_public.go @@ -0,0 +1,157 @@ +package database + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errutil" +) + +// retrieves public dashboard configuration +func (d *DashboardStore) GetPublicDashboard(uid string) (*models.PublicDashboard, *models.Dashboard, error) { + if uid == "" { + return nil, nil, models.ErrPublicDashboardIdentifierNotSet + } + + // get public dashboard + pdRes := &models.PublicDashboard{Uid: uid} + err := d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + has, err := sess.Get(pdRes) + if err != nil { + return err + } + if !has { + return models.ErrPublicDashboardNotFound + } + return nil + }) + + if err != nil { + return nil, nil, err + } + + // find dashboard + dashRes := &models.Dashboard{OrgId: pdRes.OrgId, Uid: pdRes.DashboardUid} + err = d.sqlStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + has, err := sess.Get(dashRes) + if err != nil { + return err + } + if !has { + return models.ErrPublicDashboardNotFound + } + return nil + }) + + if err != nil { + return nil, nil, err + } + + return pdRes, dashRes, err +} + +// 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() + + exists, err := sess.Get(&models.PublicDashboard{Uid: uid}) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", models.ErrPublicDashboardFailedGenerateUniqueUid +} + +// retrieves public dashboard configuration +func (d *DashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, 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 + } + + // publicDashboard + _, err = sess.Get(pdRes) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + pdc := &models.PublicDashboardConfig{ + IsPublic: dashRes.IsPublic, + PublicDashboard: *pdRes, + } + + return pdc, 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 errutil.Wrapf(err, "Failed to generate UID for public dashboard") + } + cmd.PublicDashboardConfig.PublicDashboard.Uid = uid + } + + _, err = sess.Insert(&cmd.PublicDashboardConfig.PublicDashboard) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &cmd.PublicDashboardConfig, nil +} diff --git a/pkg/services/dashboards/database/database_dashboard_public_test.go b/pkg/services/dashboards/database/database_dashboard_public_test.go new file mode 100644 index 00000000000..56f63256774 --- /dev/null +++ b/pkg/services/dashboards/database/database_dashboard_public_test.go @@ -0,0 +1,233 @@ +//go:build integration +// +build integration + +package database + +import ( + "testing" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// GetPublicDashboard +func TestGetPublicDashboard(t *testing.T) { + var sqlStore *sqlstore.SQLStore + var dashboardStore *DashboardStore + var savedDashboard *models.Dashboard + + setup := func() { + sqlStore = sqlstore.InitTestDB(t) + dashboardStore = ProvideDashboardStore(sqlStore) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + } + + 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, + }, + }, + }) + require.NoError(t, err) + + pd, d, err := dashboardStore.GetPublicDashboard("abc1234") + require.NoError(t, err) + assert.Equal(t, pd, &pdc.PublicDashboard) + assert.Equal(t, d.Uid, pdc.PublicDashboard.DashboardUid) + }) + + t.Run("returns ErrPublicDashboardNotFound with empty uid", func(t *testing.T) { + setup() + _, _, err := dashboardStore.GetPublicDashboard("") + require.Error(t, models.ErrPublicDashboardIdentifierNotSet, err) + }) + + t.Run("returns ErrPublicDashboardNotFound when PublicDashboard not found", func(t *testing.T) { + setup() + _, _, err := dashboardStore.GetPublicDashboard("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{ + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + PublicDashboardConfig: models.PublicDashboardConfig{ + IsPublic: true, + PublicDashboard: models.PublicDashboard{ + Uid: "abc1234", + DashboardUid: "nevergonnafindme", + OrgId: savedDashboard.OrgId, + }, + }, + }) + require.NoError(t, err) + _, _, err = dashboardStore.GetPublicDashboard("abc1234") + require.Error(t, models.ErrDashboardNotFound, err) + }) + +} + +// GetPublicDashboardConfig +func TestGetPublicDashboardConfig(t *testing.T) { + var sqlStore *sqlstore.SQLStore + var dashboardStore *DashboardStore + var savedDashboard *models.Dashboard + + setup := func() { + sqlStore = sqlstore.InitTestDB(t) + dashboardStore = ProvideDashboardStore(sqlStore) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + } + + t.Run("returns isPublic and set dashboardUid and orgId", func(t *testing.T) { + setup() + pdc, err := dashboardStore.GetPublicDashboardConfig(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) + }) + + t.Run("returns dashboard errDashboardIdentifierNotSet", func(t *testing.T) { + setup() + _, err := dashboardStore.GetPublicDashboardConfig(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{ + 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}", + }, + }, + }) + require.NoError(t, err) + + pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid) + require.NoError(t, err) + assert.Equal(t, resp, pdc) + }) +} + +// SavePublicDashboardConfig +func TestSavePublicDashboardConfig(t *testing.T) { + var sqlStore *sqlstore.SQLStore + var dashboardStore *DashboardStore + var savedDashboard *models.Dashboard + var savedDashboard2 *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) + savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true) + } + + t.Run("saves new public dashboard", func(t *testing.T) { + setup() + resp, err := dashboardStore.SavePublicDashboardConfig(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, + }, + }, + }) + require.NoError(t, err) + + pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid) + require.NoError(t, err) + + //verify saved response and queried response are the same + assert.Equal(t, resp, pdc) + + // verify we have a valid uid + assert.True(t, util.IsValidShortUID(pdc.PublicDashboard.Uid)) + + // verify we didn't update all dashboards + pdc2, err := dashboardStore.GetPublicDashboardConfig(savedDashboard2.OrgId, savedDashboard2.Uid) + assert.False(t, pdc2.IsPublic) + }) + + 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) + }) + + t.Run("overwrites existing public dashboard", func(t *testing.T) { + setup() + + pdUid := util.GenerateShortUID() + + // insert initial record + _, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + PublicDashboardConfig: models.PublicDashboardConfig{ + IsPublic: true, + PublicDashboard: models.PublicDashboard{ + Uid: pdUid, + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + }, + }, + }) + require.NoError(t, err) + + // update initial record + resp, err := dashboardStore.SavePublicDashboardConfig(models.SavePublicDashboardConfigCommand{ + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + PublicDashboardConfig: models.PublicDashboardConfig{ + IsPublic: false, + PublicDashboard: models.PublicDashboard{ + Uid: pdUid, + DashboardUid: savedDashboard.Uid, + OrgId: savedDashboard.OrgId, + TimeSettings: "{}", + }, + }, + }) + require.NoError(t, err) + + pdc, err := dashboardStore.GetPublicDashboardConfig(savedDashboard.OrgId, savedDashboard.Uid) + require.NoError(t, err) + assert.Equal(t, resp, pdc) + }) +} diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 662f9ad7cba..431647e3294 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -16,9 +16,9 @@ type SaveDashboardDTO struct { } type SavePublicDashboardConfigDTO struct { - Uid string + DashboardUid string OrgId int64 - PublicDashboardConfig models.PublicDashboardConfig + PublicDashboardConfig *models.PublicDashboardConfig } type DashboardSearchProjection struct { diff --git a/pkg/services/dashboards/service/dashboard_public.go b/pkg/services/dashboards/service/dashboard_public.go new file mode 100644 index 00000000000..d282fd70cc5 --- /dev/null +++ b/pkg/services/dashboards/service/dashboard_public.go @@ -0,0 +1,60 @@ +package service + +import ( + "context" + + "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) + + if err != nil { + return nil, err + } + + if pdc == nil || d == nil { + return nil, models.ErrPublicDashboardNotFound + } + + if !d.IsPublic { + return nil, models.ErrPublicDashboardNotFound + } + + // FIXME insert logic to substitute pdc.TimeSettings into d + + 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) + if err != nil { + return nil, err + } + + return pdc, nil +} + +// 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, + } + + // Eventually we want this to propagate to array of public dashboards + cmd.PublicDashboardConfig.PublicDashboard.OrgId = dto.OrgId + cmd.PublicDashboardConfig.PublicDashboard.DashboardUid = dto.DashboardUid + + pdc, err := dr.dashboardStore.SavePublicDashboardConfig(cmd) + if err != nil { + return nil, err + } + + return pdc, nil +} diff --git a/pkg/services/dashboards/service/dashboard_public_test.go b/pkg/services/dashboards/service/dashboard_public_test.go new file mode 100644 index 00000000000..a75890bf1f1 --- /dev/null +++ b/pkg/services/dashboards/service/dashboard_public_test.go @@ -0,0 +1,162 @@ +package service + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestGetPublicDashboard(t *testing.T) { + type storeResp struct { + pd *models.PublicDashboard + d *models.Dashboard + err error + } + + testCases := []struct { + name string + uid 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 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 if PublicDashboard missing", + uid: "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, + }, + } + + for _, test := range testCases { + 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) + } else { + require.NoError(t, err) + } + assert.Equal(t, test.dashResp, dashboard) + }) + } +} + +func TestSavePublicDashboard(t *testing.T) { + t.Run("gets PublicDashboard.orgId and PublicDashboard.DashboardUid set from SavePublicDashboardConfigDTO", 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, + PublicDashboardConfig: &models.PublicDashboardConfig{ + IsPublic: true, + PublicDashboard: models.PublicDashboard{ + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + }, + }, + } + + pdc, 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) + }) + + 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) + + //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, + //}, + //}, + //} + + //pdc, 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) + }) +} + +func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, title string, orgId int64, + folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + dash, err := dashboardStore.SaveDashboard(cmd) + require.NoError(t, err) + require.NotNil(t, dash) + dash.Data.Set("id", dash.Id) + dash.Data.Set("uid", dash.Uid) + return dash +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 5f117c65ad2..0ef591af11b 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -344,33 +344,6 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar return dash, 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) - if err != nil { - return nil, err - } - - return pdc, nil -} - -// 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{ - Uid: dto.Uid, - OrgId: dto.OrgId, - PublicDashboardConfig: dto.PublicDashboardConfig, - } - - pdc, err := dr.dashboardStore.SavePublicDashboardConfig(cmd) - if err != nil { - return nil, err - } - - return pdc, nil -} - // DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for // operations by the user where we want to make sure user does not delete provisioned dashboard. func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 20b34459917..c7e061b6d97 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_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 @@ -289,6 +289,38 @@ 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) + + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(string) *models.PublicDashboard); ok { + r0 = rf(uid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboard) + } + } + + var r1 *models.Dashboard + if rf, ok := ret.Get(1).(func(string) *models.Dashboard); ok { + r1 = rf(uid) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*models.Dashboard) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(string) error); ok { + r2 = rf(uid) + } else { + r2 = ret.Error(2) + } + + 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) diff --git a/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go b/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go index 97c86e5bab0..a53c80e6b6d 100644 --- a/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go @@ -8,19 +8,26 @@ func addPublicDashboardMigration(mg *Migrator) { var dashboardPublicCfgV1 = Table{ Name: "dashboard_public_config", Columns: []*Column{ - {Name: "uid", Type: DB_BigInt, IsPrimaryKey: true}, + {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}, - {Name: "time_variables", Type: DB_Text, Nullable: false}, }, 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/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx index 79a37a0769c..dd460df3d24 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx @@ -1,10 +1,27 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; +import { BackendSrv } from '@grafana/runtime'; import config from 'app/core/config'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { ShareModal } from './ShareModal'; +import { PublicDashboardConfig } from './SharePublicDashboardUtils'; + +// Mock api request +const publicDashboardconfigResp: PublicDashboardConfig = { + isPublic: true, + publicDashboard: { uid: '', dashboardUid: '' }, +}; + +const backendSrv = { + get: () => publicDashboardconfigResp, +} as unknown as BackendSrv; + +jest.mock('@grafana/runtime', () => ({ + ...(jest.requireActual('@grafana/runtime') as unknown as object), + getBackendSrv: () => backendSrv, +})); jest.mock('app/core/core', () => { return { diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx index 843fb0c760d..6363dce4035 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard.tsx @@ -1,12 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { Button, Field, Switch } from '@grafana/ui'; +import { Button, Field, Switch, Alert } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; -import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification'; +import { createErrorNotification } from 'app/core/copy/appNotification'; +import { VariableModel } from 'app/features/variables/types'; import { dispatch } from 'app/store/store'; import { - dashboardCanBePublic, + dashboardHasTemplateVariables, getPublicDashboardConfig, savePublicDashboardConfig, PublicDashboardConfig, @@ -15,46 +16,45 @@ import { ShareModalTabProps } from './types'; interface Props extends ShareModalTabProps {} -// 1. write test for dashboardCanBePublic -// 2. figure out how to disable the switch - export const SharePublicDashboard = (props: Props) => { - const [publicDashboardConfig, setPublicDashboardConfig] = useState({ isPublic: false }); const dashboardUid = props.dashboard.uid; + const [publicDashboardConfig, setPublicDashboardConfig] = useState({ + isPublic: false, + publicDashboard: { uid: '', dashboardUid }, + }); + + const [dashboardVariables, setDashboardVariables] = useState([]); useEffect(() => { - getPublicDashboardConfig(dashboardUid) - .then((pdc: PublicDashboardConfig) => { - setPublicDashboardConfig(pdc); - }) - .catch(() => { - dispatch(notifyApp(createErrorNotification('Failed to retrieve public dashboard config'))); - }); - }, [dashboardUid]); + setDashboardVariables(props.dashboard.getVariables()); + getPublicDashboardConfig(dashboardUid, setPublicDashboardConfig).catch(); + }, [props, dashboardUid]); const onSavePublicConfig = () => { - // verify dashboard can be public - if (!dashboardCanBePublic(props.dashboard)) { - dispatch(notifyApp(createErrorNotification('This dashboard cannot be made public'))); + if (dashboardHasTemplateVariables(dashboardVariables)) { + dispatch( + notifyApp(createErrorNotification('This dashboard cannot be made public because it has template variables')) + ); return; } - try { - savePublicDashboardConfig(props.dashboard.uid, publicDashboardConfig); - dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved'))); - } catch (err) { - console.error('Error while making dashboard public', err); - dispatch(notifyApp(createErrorNotification('Error making dashboard public'))); - } + savePublicDashboardConfig(props.dashboard.uid, publicDashboardConfig, setPublicDashboardConfig).catch(); }; return ( <> + {dashboardHasTemplateVariables(dashboardVariables) && ( + + This dashboard cannot be made public because it has template variables + + )} +

Public Dashboard Configuration

+ setPublicDashboardConfig((state) => { diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx index 58b1d4e3cc2..a42188b122d 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.test.tsx @@ -1,17 +1,16 @@ -import { DashboardModel } from 'app/features/dashboard/state'; +import { VariableModel } from 'app/features/variables/types'; -import { dashboardCanBePublic } from './SharePublicDashboardUtils'; +import { dashboardHasTemplateVariables } from './SharePublicDashboardUtils'; -describe('dashboardCanBePublic', () => { - it('can be public with no template variables', () => { - //@ts-ignore - const dashboard: DashboardModel = { templating: { list: [] } }; - expect(dashboardCanBePublic(dashboard)).toBe(true); +describe('dashboardHasTemplateVariables', () => { + it('false', () => { + let variables: VariableModel[] = []; + expect(dashboardHasTemplateVariables(variables)).toBe(false); }); - it('cannot be public with template variables', () => { + it('true', () => { //@ts-ignore - const dashboard: DashboardModel = { templating: { list: [{}] } }; - expect(dashboardCanBePublic(dashboard)).toBe(false); + let variables: VariableModel[] = ['a']; + expect(dashboardHasTemplateVariables(variables)).toBe(true); }); }); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts index 6213ddfae75..bd6a0dc60eb 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboardUtils.ts @@ -1,21 +1,43 @@ import { getBackendSrv } from '@grafana/runtime'; -import { DashboardModel } from 'app/features/dashboard/state'; +import { notifyApp } from 'app/core/actions'; +import { createSuccessNotification } from 'app/core/copy/appNotification'; +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 DashboardResponse { + dashboard: DashboardDataDTO; + meta: DashboardMeta; } -export const dashboardCanBePublic = (dashboard: DashboardModel): boolean => { - return dashboard?.templating?.list.length === 0; +export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => { + return variables.length > 0; }; -export const getPublicDashboardConfig = async (dashboardUid: string) => { +export const getPublicDashboardConfig = async ( + dashboardUid: string, + setPublicDashboardConfig: React.Dispatch> +) => { const url = `/api/dashboards/uid/${dashboardUid}/public-config`; - return getBackendSrv().get(url); + const pdResp: PublicDashboardConfig = await getBackendSrv().get(url); + setPublicDashboardConfig(pdResp); }; -export const savePublicDashboardConfig = async (dashboardUid: string, conf: PublicDashboardConfig) => { - const payload = { isPublic: conf.isPublic }; +export const savePublicDashboardConfig = async ( + dashboardUid: string, + publicDashboardConfig: PublicDashboardConfig, + setPublicDashboardConfig: Function +) => { const url = `/api/dashboards/uid/${dashboardUid}/public-config`; - return getBackendSrv().post(url, payload); + const pdResp: PublicDashboardConfig = await getBackendSrv().post(url, publicDashboardConfig); + dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved'))); + setPublicDashboardConfig(pdResp); };