publicdashboards: split create/update api paths (#57940)

This PR splits the create and update paths for public dashboards and includes assorted refactors toward a proper REST API. Additionally, we removed the concept of a "public dashboard config" in favor of "public dashboard" 

Co-authored-by: juanicabanas <juan.cabanas@grafana.com>
Co-authored-by: Ezequiel Victorero <ezequiel.victorero@grafana.com>
This commit is contained in:
Jeff Levin 2022-11-03 11:30:12 -08:00 committed by GitHub
parent 0367f61bb3
commit 6fcc5b42c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 996 additions and 612 deletions

View File

@ -8,7 +8,7 @@ e2e.scenario({
skipScenario: false,
scenario: () => {
// Opening a dashboard without template variables
e2e().intercept('/api/ds/query').as('query');
e2e().intercept('POST', '/api/ds/query').as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
e2e().wait('@query');
@ -16,9 +16,7 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.shareButton().click();
// Select public dashboards tab
e2e().intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e().wait('@query-public-dashboard');
// Saving button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.disabled');
@ -57,7 +55,7 @@ e2e.scenario({
skipScenario: false,
scenario: () => {
// Opening a dashboard without template variables
e2e().intercept('/api/ds/query').as('query');
e2e().intercept('POST', '/api/ds/query').as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
e2e().wait('@query');
@ -125,9 +123,9 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true });
// Save public dashboard
e2e().intercept('POST', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('save');
e2e().intercept('PUT', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().click();
e2e().wait('@save');
e2e().wait('@update');
// Url should be hidden
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('not.exist');

View File

@ -14,9 +14,7 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.shareButton().click();
// Select public dashboards tab
e2e().intercept('GET', '/api/dashboards/uid/HYaGDGIMk/public-dashboards').as('query-public-config');
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e().wait('@query-public-config');
// Warning Alert dashboard cannot be made public because it has template variables
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');

View File

@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
pref "github.com/grafana/grafana/pkg/services/preference"
publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
@ -100,14 +101,23 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
}
var (
hasPublicDashboard bool
err error
hasPublicDashboard = false
publicDashboardEnabled = false
err error
)
// If public dashboards is enabled and we have a public dashboard, update meta
// values
if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) {
hasPublicDashboard, err = hs.PublicDashboardsApi.PublicDashboardService.ExistsEnabledByDashboardUid(c.Req.Context(), dash.Uid)
if err != nil {
publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, dash.Uid)
if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) {
return response.Error(500, "Error while retrieving public dashboards", err)
}
if publicDashboard != nil {
hasPublicDashboard = true
publicDashboardEnabled = publicDashboard.IsEnabled
}
}
// When dash contains only keys id, uid that means dashboard data is not valid and json decode failed.
@ -172,7 +182,8 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
Url: dash.GetUrl(),
FolderTitle: "General",
AnnotationsPermissions: annotationPermissions,
PublicDashboardEnabled: hasPublicDashboard,
PublicDashboardEnabled: publicDashboardEnabled,
HasPublicDashboard: hasPublicDashboard,
}
// lookup folder title

View File

@ -32,6 +32,7 @@ type DashboardMeta struct {
Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
HasPublicDashboard bool `json:"hasPublicDashboard"`
PublicDashboardAccessToken string `json:"publicDashboardAccessToken"`
PublicDashboardUID string `json:"publicDashboardUid"`
PublicDashboardEnabled bool `json:"publicDashboardEnabled"`

View File

@ -257,7 +257,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
AccessToken: "an-access-token",
},
}
err := publicDashboardStore.Save(context.Background(), cmd)
_, err := publicDashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token")
require.NotNil(t, pubdashConfig)
@ -292,7 +292,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
AccessToken: "an-access-token",
},
}
err := publicDashboardStore.Save(context.Background(), cmd)
_, err := publicDashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token")
require.NotNil(t, pubdashConfig)

View File

@ -15,8 +15,8 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards"
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@ -73,10 +73,15 @@ func (api *Api) RegisterAPIEndpoints() {
auth(middleware.ReqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, uidScope)),
routing.Wrap(api.GetPublicDashboard))
// Create/Update Public Dashboard
// Create Public Dashboard
api.RouteRegister.Post("/api/dashboards/uid/:dashboardUid/public-dashboards",
auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
routing.Wrap(api.SavePublicDashboard))
routing.Wrap(api.CreatePublicDashboard))
// Update Public Dashboard
api.RouteRegister.Put("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
routing.Wrap(api.UpdatePublicDashboard))
// Delete Public dashboard
api.RouteRegister.Delete("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
@ -94,59 +99,103 @@ func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, resp)
}
// GetPublicDashboard Gets public dashboard configuration for dashboard
// GET /api/dashboards/uid/:uid/public-config
// GetPublicDashboard Gets public dashboard for dashboard
// GET /api/dashboards/uid/:uid/public-dashboards
func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
// exit if we don't have a valid dashboardUid
dashboardUid := web.Params(c.Req)[":dashboardUid"]
if dashboardUid == "" || !util.IsValidShortUID(dashboardUid) {
if !tokens.IsValidShortUID(dashboardUid) {
api.handleError(c.Req.Context(), http.StatusBadRequest, "GetPublicDashboard: no valid dashboardUid", dashboards.ErrDashboardIdentifierNotSet)
}
pdc, err := api.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, web.Params(c.Req)[":dashboardUid"])
pd, err := api.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, web.Params(c.Req)[":dashboardUid"])
if err != nil {
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "GetPublicDashboardConfig: failed to get public dashboard config", err)
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "GetPublicDashboard: failed to get public dashboard ", err)
}
return response.JSON(http.StatusOK, pdc)
if pd == nil {
return api.handleError(c.Req.Context(), http.StatusNotFound, "GetPublicDashboard: public dashboard not found", ErrPublicDashboardNotFound)
}
return response.JSON(http.StatusOK, pd)
}
// SavePublicDashboard Sets public dashboard configuration for dashboard
// POST /api/dashboards/uid/:uid/public-config
func (api *Api) SavePublicDashboard(c *models.ReqContext) response.Response {
// CreatePublicDashboard Sets public dashboard for dashboard
// POST /api/dashboards/uid/:uid/public-dashboards
func (api *Api) CreatePublicDashboard(c *models.ReqContext) response.Response {
// exit if we don't have a valid dashboardUid
dashboardUid := web.Params(c.Req)[":dashboardUid"]
if dashboardUid == "" || !util.IsValidShortUID(dashboardUid) {
api.handleError(c.Req.Context(), http.StatusBadRequest, "SavePublicDashboard: invalid dashboardUid", dashboards.ErrDashboardIdentifierNotSet)
if !tokens.IsValidShortUID(dashboardUid) {
return api.handleError(c.Req.Context(), http.StatusBadRequest, "CreatePublicDashboard: invalid dashboardUid", dashboards.ErrDashboardIdentifierInvalid)
}
pubdash := &PublicDashboard{}
if err := web.Bind(c.Req, pubdash); err != nil {
return response.Error(http.StatusBadRequest, "SavePublicDashboard: bad request data", err)
pd := &PublicDashboard{}
if err := web.Bind(c.Req, pd); err != nil {
return api.handleError(c.Req.Context(), http.StatusBadRequest, "CreatePublicDashboard: bad request data", err)
}
// Always set the orgID and userID from the session
pubdash.OrgId = c.OrgID
pd.OrgId = c.OrgID
dto := SavePublicDashboardDTO{
UserId: c.UserID,
OrgId: c.OrgID,
DashboardUid: dashboardUid,
PublicDashboard: pubdash,
PublicDashboard: pd,
}
//Create the public dashboard
pd, err := api.PublicDashboardService.Create(c.Req.Context(), c.SignedInUser, &dto)
if err != nil {
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "CreatePublicDashboard: failed to create public dashboard", err)
}
return response.JSON(http.StatusOK, pd)
}
// UpdatePublicDashboard Sets public dashboard for dashboard
// PUT /api/dashboards/uid/:uid/public-dashboards
func (api *Api) UpdatePublicDashboard(c *models.ReqContext) response.Response {
// exit if we don't have a valid dashboardUid
dashboardUid := web.Params(c.Req)[":dashboardUid"]
if !tokens.IsValidShortUID(dashboardUid) {
return api.handleError(c.Req.Context(), http.StatusBadRequest, "UpdatePublicDashboard: invalid dashboardUid", dashboards.ErrDashboardIdentifierInvalid)
}
uid := web.Params(c.Req)[":uid"]
if !tokens.IsValidShortUID(uid) {
return api.handleError(c.Req.Context(), http.StatusBadRequest, "UpdatePublicDashboard: invalid public dashboard uid", ErrPublicDashboardIdentifierNotSet)
}
pd := &PublicDashboard{}
if err := web.Bind(c.Req, pd); err != nil {
return api.handleError(c.Req.Context(), http.StatusBadRequest, "UpdatePublicDashboard: bad request data", err)
}
// Always set the orgID and userID from the session
pd.OrgId = c.OrgID
pd.Uid = uid
dto := SavePublicDashboardDTO{
UserId: c.UserID,
OrgId: c.OrgID,
DashboardUid: dashboardUid,
PublicDashboard: pd,
}
// Save the public dashboard
pubdash, err := api.PublicDashboardService.Save(c.Req.Context(), c.SignedInUser, &dto)
pd, err := api.PublicDashboardService.Update(c.Req.Context(), c.SignedInUser, &dto)
if err != nil {
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "SavePublicDashboardConfig: failed to save public dashboard configuration", err)
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "UpdatePublicDashboard: failed to update public dashboard", err)
}
return response.JSON(http.StatusOK, pubdash)
return response.JSON(http.StatusOK, pd)
}
// Delete a public dashboard
// DELETE /api/dashboards/uid/:dashboardUid/public-dashboards/:uid
func (api *Api) DeletePublicDashboard(c *models.ReqContext) response.Response {
uid := web.Params(c.Req)[":uid"]
if uid == "" || !util.IsValidShortUID(uid) {
if !tokens.IsValidShortUID(uid) {
return api.handleError(c.Req.Context(), http.StatusBadRequest, "DeletePublicDashboard: invalid dashboard uid", dashboards.ErrDashboardIdentifierNotSet)
}
@ -171,7 +220,6 @@ func (api *Api) handleError(ctx context.Context, code int, message string, err e
return response.Error(publicDashboardErr.StatusCode, publicDashboardErr.Error(), publicDashboardErr)
}
// handle dashboard errors as well
var dashboardErr dashboards.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr)

View File

@ -57,10 +57,20 @@ func TestAPIFeatureFlag(t *testing.T) {
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
{
Name: "API: Save Public Dashboard",
Name: "API: Create Public Dashboard",
Method: http.MethodPost,
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
{
Name: "API: Update Public Dashboard",
Method: http.MethodPut,
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
{
Name: "API: Delete Public Dashboard",
Method: http.MethodDelete,
Path: "/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
},
}
for _, test := range testCases {
@ -148,6 +158,354 @@ func TestAPIListPublicDashboard(t *testing.T) {
}
}
func TestAPIGetPublicDashboard(t *testing.T) {
pubdash := &PublicDashboard{IsEnabled: true}
testCases := []struct {
Name string
DashboardUid string
ExpectedHttpResponse int
PublicDashboardResult *PublicDashboard
PublicDashboardErr error
User *user.SignedInUser
AccessControlEnabled bool
ShouldCallService bool
}{
{
Name: "retrieves public dashboard when dashboard is found",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 404 when dashboard not found",
DashboardUid: "77777",
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardErr: dashboards.ErrDashboardNotFound,
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 500 when internal server error",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusInternalServerError,
PublicDashboardResult: nil,
PublicDashboardErr: errors.New("database broken"),
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "retrieves public dashboard when dashboard is found RBAC on",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userViewerRBAC,
AccessControlEnabled: true,
ShouldCallService: true,
},
{
Name: "returns 403 when no permissions RBAC on",
ExpectedHttpResponse: http.StatusForbidden,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userViewer,
AccessControlEnabled: true,
ShouldCallService: false,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
if test.ShouldCallService {
service.On("FindByDashboardUid", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr)
}
cfg := setting.NewCfg()
cfg.RBACEnabled = test.AccessControlEnabled
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI(
testServer,
http.MethodGet,
"/api/dashboards/uid/1/public-dashboards",
nil,
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if response.Code == http.StatusOK {
var pdcResp PublicDashboard
err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
require.NoError(t, err)
assert.Equal(t, test.PublicDashboardResult, &pdcResp)
}
})
}
}
func TestApiCreatePublicDashboard(t *testing.T) {
testCases := []struct {
Name string
DashboardUid string
publicDashboard *PublicDashboard
ExpectedHttpResponse int
SaveDashboardErr error
User *user.SignedInUser
AccessControlEnabled bool
ShouldCallService bool
}{
{
Name: "returns 200 when update persists",
DashboardUid: "1",
publicDashboard: &PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
SaveDashboardErr: nil,
User: userAdmin,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 500 when not persisted",
ExpectedHttpResponse: http.StatusInternalServerError,
publicDashboard: &PublicDashboard{},
SaveDashboardErr: errors.New("backend failed to save"),
User: userAdmin,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 404 when dashboard not found",
ExpectedHttpResponse: http.StatusNotFound,
publicDashboard: &PublicDashboard{},
SaveDashboardErr: dashboards.ErrDashboardNotFound,
User: userAdmin,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 200 when update persists RBAC on",
DashboardUid: "1",
publicDashboard: &PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
SaveDashboardErr: nil,
User: userAdminRBAC,
AccessControlEnabled: true,
ShouldCallService: true,
},
{
Name: "returns 403 when no permissions",
ExpectedHttpResponse: http.StatusForbidden,
publicDashboard: &PublicDashboard{IsEnabled: true},
SaveDashboardErr: nil,
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: false,
},
{
Name: "returns 403 when no permissions RBAC on",
ExpectedHttpResponse: http.StatusForbidden,
publicDashboard: &PublicDashboard{IsEnabled: true},
SaveDashboardErr: nil,
User: userAdmin,
AccessControlEnabled: true,
ShouldCallService: false,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
// this is to avoid AssertExpectations fail at t.Cleanup when the middleware returns before calling the service
if test.ShouldCallService {
service.On("Create", mock.Anything, mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardDTO")).
Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
}
cfg := setting.NewCfg()
cfg.RBACEnabled = test.AccessControlEnabled
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI(
testServer,
http.MethodPost,
"/api/dashboards/uid/1/public-dashboards",
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.publicDashboard)
require.NoError(t, err)
assert.Equal(t, string(val), response.Body.String())
}
})
}
}
func TestAPIUpdatePublicDashboard(t *testing.T) {
dashboardUid := "abc1234"
publicDashboardUid := "1234asdfasdf"
adminUser := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {dashboards.ScopeDashboardsAll}}}}
userEditorPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {fmt.Sprintf("dashboards:uid:%s", dashboardUid)}}}}
userEditorAnotherPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {"another-uid"}}}}
testCases := []struct {
Name string
User *user.SignedInUser
DashboardUid string
PublicDashboardUid string
PublicDashboardRes *PublicDashboard
PublicDashboardErr error
ExpectedHttpResponse int
ShouldCallService bool
}{
{
Name: "Invalid dashboardUid",
User: adminUser,
DashboardUid: "",
PublicDashboardUid: "",
PublicDashboardRes: nil,
PublicDashboardErr: dashboards.ErrDashboardIdentifierInvalid,
ExpectedHttpResponse: http.StatusNotFound,
ShouldCallService: false,
},
{
Name: "Invalid public dashboard uid",
User: adminUser,
DashboardUid: dashboardUid,
PublicDashboardUid: "",
PublicDashboardRes: nil,
PublicDashboardErr: ErrPublicDashboardNotFound,
ExpectedHttpResponse: http.StatusNotFound,
ShouldCallService: false,
},
{
Name: "Service Error",
User: adminUser,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
PublicDashboardRes: nil,
PublicDashboardErr: dashboards.ErrDashboardNotFound,
ExpectedHttpResponse: http.StatusNotFound,
ShouldCallService: true,
},
{
Name: "Success",
User: adminUser,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
PublicDashboardRes: &PublicDashboard{Uid: "success"},
PublicDashboardErr: nil,
ExpectedHttpResponse: http.StatusOK,
ShouldCallService: true,
},
// permissions
{
Name: "User can update this public dashboard",
User: userEditorPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
PublicDashboardRes: &PublicDashboard{Uid: "success"},
PublicDashboardErr: nil,
ExpectedHttpResponse: http.StatusOK,
ShouldCallService: true,
},
{
Name: "User has permissions on another dashboard",
User: userEditorAnotherPublicDashboard,
PublicDashboardUid: publicDashboardUid,
ExpectedHttpResponse: http.StatusForbidden,
ShouldCallService: false,
},
{
Name: "Viewer cannot update any dashboard",
User: userViewer,
PublicDashboardUid: publicDashboardUid,
ExpectedHttpResponse: http.StatusForbidden,
ShouldCallService: false,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
if test.ShouldCallService {
service.On("Update", mock.Anything, mock.Anything, mock.Anything).
Return(test.PublicDashboardRes, test.PublicDashboardErr)
}
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
url := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid)
body := strings.NewReader(fmt.Sprintf(`{ "uid": "%s"}`, test.PublicDashboardUid))
response := callAPI(testServer, http.MethodPut, url, body, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
// check whether service called
if !test.ShouldCallService {
service.AssertNotCalled(t, "Update")
}
fmt.Println(response.Body.String())
// check response
if response.Code == http.StatusOK {
val, err := json.Marshal(test.PublicDashboardRes)
require.NoError(t, err)
assert.Equal(t, string(val), response.Body.String())
// verify 4XXs except 403 && 404
} else if test.ExpectedHttpResponse > 200 &&
test.ExpectedHttpResponse != 403 &&
test.ExpectedHttpResponse != 404 {
var errResp JsonErrResponse
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, test.PublicDashboardErr.Error(), errResp.Error)
}
})
}
}
func TestAPIDeletePublicDashboard(t *testing.T) {
dashboardUid := "abc1234"
publicDashboardUid := "1234asdfasdf"
@ -275,219 +633,3 @@ func TestAPIDeletePublicDashboard(t *testing.T) {
})
}
}
func TestAPIGetPublicDashboard(t *testing.T) {
pubdash := &PublicDashboard{IsEnabled: true}
testCases := []struct {
Name string
DashboardUid string
ExpectedHttpResponse int
PublicDashboardResult *PublicDashboard
PublicDashboardErr error
User *user.SignedInUser
AccessControlEnabled bool
ShouldCallService bool
}{
{
Name: "retrieves public dashboard when dashboard is found",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 404 when dashboard not found",
DashboardUid: "77777",
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardErr: dashboards.ErrDashboardNotFound,
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 500 when internal server error",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusInternalServerError,
PublicDashboardResult: nil,
PublicDashboardErr: errors.New("database broken"),
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "retrieves public dashboard when dashboard is found RBAC on",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userViewerRBAC,
AccessControlEnabled: true,
ShouldCallService: true,
},
{
Name: "returns 403 when no permissions RBAC on",
ExpectedHttpResponse: http.StatusForbidden,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userViewer,
AccessControlEnabled: true,
ShouldCallService: false,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
if test.ShouldCallService {
service.On("FindByDashboardUid", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr)
}
cfg := setting.NewCfg()
cfg.RBACEnabled = test.AccessControlEnabled
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI(
testServer,
http.MethodGet,
"/api/dashboards/uid/1/public-dashboards",
nil,
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if response.Code == http.StatusOK {
var pdcResp PublicDashboard
err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
require.NoError(t, err)
assert.Equal(t, test.PublicDashboardResult, &pdcResp)
}
})
}
}
func TestApiSavePublicDashboard(t *testing.T) {
testCases := []struct {
Name string
DashboardUid string
publicDashboard *PublicDashboard
ExpectedHttpResponse int
SaveDashboardErr error
User *user.SignedInUser
AccessControlEnabled bool
ShouldCallService bool
}{
{
Name: "returns 200 when update persists",
DashboardUid: "1",
publicDashboard: &PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
SaveDashboardErr: nil,
User: userAdmin,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 500 when not persisted",
ExpectedHttpResponse: http.StatusInternalServerError,
publicDashboard: &PublicDashboard{},
SaveDashboardErr: errors.New("backend failed to save"),
User: userAdmin,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 404 when dashboard not found",
ExpectedHttpResponse: http.StatusNotFound,
publicDashboard: &PublicDashboard{},
SaveDashboardErr: dashboards.ErrDashboardNotFound,
User: userAdmin,
AccessControlEnabled: false,
ShouldCallService: true,
},
{
Name: "returns 200 when update persists RBAC on",
DashboardUid: "1",
publicDashboard: &PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
SaveDashboardErr: nil,
User: userAdminRBAC,
AccessControlEnabled: true,
ShouldCallService: true,
},
{
Name: "returns 403 when no permissions",
ExpectedHttpResponse: http.StatusForbidden,
publicDashboard: &PublicDashboard{IsEnabled: true},
SaveDashboardErr: nil,
User: userViewer,
AccessControlEnabled: false,
ShouldCallService: false,
},
{
Name: "returns 403 when no permissions RBAC on",
ExpectedHttpResponse: http.StatusForbidden,
publicDashboard: &PublicDashboard{IsEnabled: true},
SaveDashboardErr: nil,
User: userAdmin,
AccessControlEnabled: true,
ShouldCallService: false,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
// this is to avoid AssertExpectations fail at t.Cleanup when the middleware returns before calling the service
if test.ShouldCallService {
service.On("Save", mock.Anything, mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardDTO")).
Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
}
cfg := setting.NewCfg()
cfg.RBACEnabled = test.AccessControlEnabled
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI(
testServer,
http.MethodPost,
"/api/dashboards/uid/1/public-dashboards",
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.publicDashboard)
require.NoError(t, err)
assert.Equal(t, string(val), response.Body.String())
}
})
}
}

View File

@ -316,7 +316,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
ac := acmock.New()
cfg.RBACEnabled = false
service := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService, ac)
pubdash, err := service.Save(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
pubdash, err := service.Create(context.Background(), &user.SignedInUser{}, savePubDashboardCmd)
require.NoError(t, err)
// setup test server

View File

@ -66,11 +66,15 @@ func (d *PublicDashboardStoreImpl) FindDashboard(ctx context.Context, orgId int6
return err
})
if err != nil {
return nil, err
}
if !found {
return nil, nil
}
return dashboard, err
return dashboard, nil
}
// Find Returns public dashboard by Uid or nil if not found
@ -80,10 +84,10 @@ func (d *PublicDashboardStoreImpl) Find(ctx context.Context, uid string) (*Publi
}
var found bool
pdRes := &PublicDashboard{Uid: uid}
publicDashboard := &PublicDashboard{Uid: uid}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
found, err = sess.Get(pdRes)
found, err = sess.Get(publicDashboard)
return err
})
@ -95,7 +99,7 @@ func (d *PublicDashboardStoreImpl) Find(ctx context.Context, uid string) (*Publi
return nil, nil
}
return pdRes, err
return publicDashboard, nil
}
// FindByAccessToken Returns public dashboard by access token or nil if not found
@ -105,10 +109,10 @@ func (d *PublicDashboardStoreImpl) FindByAccessToken(ctx context.Context, access
}
var found bool
pdRes := &PublicDashboard{AccessToken: accessToken}
publicDashboard := &PublicDashboard{AccessToken: accessToken}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
found, err = sess.Get(pdRes)
found, err = sess.Get(publicDashboard)
return err
})
@ -120,7 +124,7 @@ func (d *PublicDashboardStoreImpl) FindByAccessToken(ctx context.Context, access
return nil, nil
}
return pdRes, err
return publicDashboard, nil
}
// FindByDashboardUid Retrieves public dashboard by dashboard uid or nil if not found
@ -128,7 +132,6 @@ func (d *PublicDashboardStoreImpl) FindByDashboardUid(ctx context.Context, orgId
if dashboardUid == "" || orgId == 0 {
return nil, nil
}
var found bool
publicDashboard := &PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
@ -149,67 +152,7 @@ func (d *PublicDashboardStoreImpl) FindByDashboardUid(ctx context.Context, orgId
return nil, nil
}
return publicDashboard, err
}
// Save Persists public dashboard
func (d *PublicDashboardStoreImpl) Save(ctx context.Context, cmd SavePublicDashboardCommand) error {
if cmd.PublicDashboard.DashboardUid == "" {
return dashboards.ErrDashboardIdentifierNotSet
}
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
_, err := sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard)
if err != nil {
return err
}
return nil
})
return err
}
// Update updates existing public dashboard
func (d *PublicDashboardStoreImpl) Update(ctx context.Context, cmd SavePublicDashboardCommand) error {
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
timeSettingsJSON, err := json.Marshal(cmd.PublicDashboard.TimeSettings)
if err != nil {
return err
}
_, err = sess.Exec("UPDATE dashboard_public SET is_enabled = ?, annotations_enabled = ?, time_settings = ?, updated_by = ?, updated_at = ? WHERE uid = ?",
cmd.PublicDashboard.IsEnabled,
cmd.PublicDashboard.AnnotationsEnabled,
string(timeSettingsJSON),
cmd.PublicDashboard.UpdatedBy,
cmd.PublicDashboard.UpdatedAt.UTC().Format("2006-01-02 15:04:05"),
cmd.PublicDashboard.Uid)
if err != nil {
return err
}
return nil
})
return err
}
func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, orgId int64, uid string) (int64, error) {
dashboard := &PublicDashboard{OrgId: orgId, Uid: uid}
var affectedRows int64
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
affectedRows, err = sess.Delete(dashboard)
if err != nil {
return err
}
return nil
})
return affectedRows, err
return publicDashboard, nil
}
// ExistsEnabledByDashboardUid Responds true if there is an enabled public dashboard for a dashboard uid
@ -264,3 +207,62 @@ func (d *PublicDashboardStoreImpl) GetOrgIdByAccessToken(ctx context.Context, ac
return orgId, err
}
// Creates a public dashboard
func (d *PublicDashboardStoreImpl) Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) {
if cmd.PublicDashboard.DashboardUid == "" {
return 0, dashboards.ErrDashboardIdentifierNotSet
}
var affectedRows int64
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
affectedRows, err = sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard)
return err
})
return affectedRows, err
}
// Updates existing public dashboard
func (d *PublicDashboardStoreImpl) Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) {
var affectedRows int64
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
timeSettingsJSON, err := json.Marshal(cmd.PublicDashboard.TimeSettings)
if err != nil {
return err
}
sqlResult, err := sess.Exec("UPDATE dashboard_public SET is_enabled = ?, annotations_enabled = ?, time_settings = ?, updated_by = ?, updated_at = ? WHERE uid = ?",
cmd.PublicDashboard.IsEnabled,
cmd.PublicDashboard.AnnotationsEnabled,
string(timeSettingsJSON),
cmd.PublicDashboard.UpdatedBy,
cmd.PublicDashboard.UpdatedAt.UTC().Format("2006-01-02 15:04:05"),
cmd.PublicDashboard.Uid)
if err != nil {
return err
}
affectedRows, err = sqlResult.RowsAffected()
return err
})
return affectedRows, err
}
// Deletes a public dashboard
func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, orgId int64, uid string) (int64, error) {
dashboard := &PublicDashboard{OrgId: orgId, Uid: uid}
var affectedRows int64
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
affectedRows, err = sess.Delete(dashboard)
return err
})
return affectedRows, err
}

View File

@ -103,7 +103,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) {
t.Run("ExistsEnabledByAccessToken will return true when at least one public dashboard has a matching access token", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
_, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc123",
@ -125,7 +125,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) {
t.Run("ExistsEnabledByAccessToken will return false when IsEnabled=false", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
_, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: false,
Uid: "abc123",
@ -171,7 +171,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) {
t.Run("ExistsEnabledByDashboardUid Will return true when dashboard has at least one enabled public dashboard", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
_, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc123",
@ -193,7 +193,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) {
t.Run("ExistsEnabledByDashboardUid will return false when dashboard has public dashboards but they are not enabled", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
_, err := publicdashboardStore.Create(context.Background(), SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: false,
Uid: "abc123",
@ -257,7 +257,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) {
}
// insert test public dashboard
err := publicdashboardStore.Save(context.Background(), cmd)
_, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
// retrieve from db
@ -320,7 +320,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) {
}
// insert test public dashboard
err := publicdashboardStore.Save(context.Background(), cmd)
_, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
// retrieve from db
@ -338,7 +338,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) {
})
}
func TestIntegrationSavePublicDashboard(t *testing.T) {
func TestIntegrationCreatePublicDashboard(t *testing.T) {
var sqlStore db.DB
var cfg *setting.Cfg
var dashboardStore *dashboardsDB.DashboardStore
@ -357,7 +357,7 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
t.Run("saves new public dashboard", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
AnnotationsEnabled: true,
@ -369,14 +369,14 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
}
affectedRows, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
assert.EqualValues(t, affectedRows, 1)
pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
require.NoError(t, err)
// verify we have a valid uid
assert.True(t, util.IsValidShortUID(pubdash.Uid))
assert.Equal(t, pubdash.AccessToken, "NOTAREALUUID")
// verify we didn't update all dashboards
pubdash2, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid)
@ -386,7 +386,7 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
t.Run("guards from saving without dashboardUid", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "pubdash-uid",
@ -397,9 +397,11 @@ func TestIntegrationSavePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
}
affectedRows, err := publicdashboardStore.Create(context.Background(), cmd)
require.Error(t, err)
assert.Equal(t, err, dashboards.ErrDashboardIdentifierNotSet)
assert.EqualValues(t, affectedRows, 0)
})
}
@ -423,7 +425,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
setup()
pdUid := "asdf1234"
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: pdUid,
DashboardUid: savedDashboard.Uid,
@ -434,12 +436,14 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "NOTAREALUUID",
},
})
}
affectedRows, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
assert.EqualValues(t, affectedRows, 1)
// inserting two different public dashboards to test update works and only affect the desired pd by uid
anotherPdUid := "anotherUid"
err = publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
cmd = SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: anotherPdUid,
DashboardUid: anotherSavedDashboard.Uid,
@ -450,8 +454,11 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
CreatedBy: 7,
AccessToken: "fakeaccesstoken",
},
})
}
affectedRows, err = publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
assert.EqualValues(t, affectedRows, 1)
updatedPublicDashboard := PublicDashboard{
Uid: pdUid,
@ -463,11 +470,12 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
UpdatedAt: time.Now().UTC().Round(time.Second),
UpdatedBy: 8,
}
// update initial record
err = publicdashboardStore.Update(context.Background(), SavePublicDashboardCommand{
PublicDashboard: updatedPublicDashboard,
})
cmd = SavePublicDashboardCommand{PublicDashboard: updatedPublicDashboard}
rowsAffected, err := publicdashboardStore.Update(context.Background(), cmd)
require.NoError(t, err)
assert.EqualValues(t, rowsAffected, 1)
// updated dashboard should have changed
pdRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid)
@ -503,8 +511,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
}
t.Run("GetOrgIdByAccessToken will OrgId when enabled", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: true,
Uid: "abc123",
@ -514,7 +521,8 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
CreatedBy: 7,
AccessToken: "accessToken",
},
})
}
_, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken")
@ -525,8 +533,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
t.Run("GetOrgIdByAccessToken will return 0 when IsEnabled=false", func(t *testing.T) {
setup()
err := publicdashboardStore.Save(context.Background(), SavePublicDashboardCommand{
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
IsEnabled: false,
Uid: "abc123",
@ -536,8 +543,11 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
CreatedBy: 7,
AccessToken: "accessToken",
},
})
}
_, err := publicdashboardStore.Create(context.Background(), cmd)
require.NoError(t, err)
orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken")
require.NoError(t, err)
assert.NotEqual(t, savedDashboard.OrgId, orgId)
@ -634,8 +644,9 @@ func insertPublicDashboard(t *testing.T, publicdashboardStore *PublicDashboardSt
},
}
err = publicdashboardStore.Save(ctx, cmd)
affectedRows, err := publicdashboardStore.Create(ctx, cmd)
require.NoError(t, err)
assert.EqualValues(t, affectedRows, 1)
pubdash, err := publicdashboardStore.Find(ctx, uid)
require.NoError(t, err)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/util"
)
// GenerateAccessToken generates an uuid formatted without dashes to use as access token
@ -20,3 +21,9 @@ func IsValidAccessToken(token string) bool {
_, err := uuid.Parse(token)
return err == nil
}
// IsValidShortUID checks that the uid is not blank and contains valid
// characters. Wraps utils.IsValidShortUID
func IsValidShortUID(uid string) bool {
return uid != "" && util.IsValidShortUID(uid)
}

View File

@ -36,3 +36,19 @@ func TestValidAccessToken(t *testing.T) {
assert.False(t, IsValidAccessToken("0123456789012345678901234567890123456789"))
})
}
// we just check base cases since this wraps utils.IsValidShortUID which has
// test coverage
func TestValidUid(t *testing.T) {
t.Run("true", func(t *testing.T) {
assert.True(t, IsValidShortUID("afqrz7jZZ"))
})
t.Run("false when blank", func(t *testing.T) {
assert.False(t, IsValidShortUID(""))
})
t.Run("false when invalid chars", func(t *testing.T) {
assert.False(t, IsValidShortUID("afqrz7j%%"))
})
}

View File

@ -25,6 +25,29 @@ type FakePublicDashboardService struct {
mock.Mock
}
// Create provides a mock function with given fields: ctx, u, dto
func (_m *FakePublicDashboardService) Create(ctx context.Context, u *user.SignedInUser, dto *models.SavePublicDashboardDTO) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, u, dto)
var r0 *models.PublicDashboard
if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, *models.SavePublicDashboardDTO) *models.PublicDashboard); ok {
r0 = rf(ctx, u, dto)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.PublicDashboard)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, *models.SavePublicDashboardDTO) error); ok {
r1 = rf(ctx, u, dto)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, orgId, uid
func (_m *FakePublicDashboardService) Delete(ctx context.Context, orgId int64, uid string) error {
ret := _m.Called(ctx, orgId, uid)
@ -312,8 +335,8 @@ func (_m *FakePublicDashboardService) NewPublicDashboardUid(ctx context.Context)
return r0, r1
}
// Save provides a mock function with given fields: ctx, u, dto
func (_m *FakePublicDashboardService) Save(ctx context.Context, u *user.SignedInUser, dto *models.SavePublicDashboardDTO) (*models.PublicDashboard, error) {
// Update provides a mock function with given fields: ctx, u, dto
func (_m *FakePublicDashboardService) Update(ctx context.Context, u *user.SignedInUser, dto *models.SavePublicDashboardDTO) (*models.PublicDashboard, error) {
ret := _m.Called(ctx, u, dto)
var r0 *models.PublicDashboard

View File

@ -18,6 +18,27 @@ type FakePublicDashboardStore struct {
mock.Mock
}
// Create provides a mock function with given fields: ctx, cmd
func (_m *FakePublicDashboardStore) Create(ctx context.Context, cmd models.SavePublicDashboardCommand) (int64, error) {
ret := _m.Called(ctx, cmd)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) int64); ok {
r0 = rf(ctx, cmd)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
r1 = rf(ctx, cmd)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: ctx, orgId, uid
func (_m *FakePublicDashboardStore) Delete(ctx context.Context, orgId int64, uid string) (int64, error) {
ret := _m.Called(ctx, orgId, uid)
@ -217,32 +238,25 @@ func (_m *FakePublicDashboardStore) GetOrgIdByAccessToken(ctx context.Context, a
return r0, r1
}
// Save provides a mock function with given fields: ctx, cmd
func (_m *FakePublicDashboardStore) Save(ctx context.Context, cmd models.SavePublicDashboardCommand) error {
ret := _m.Called(ctx, cmd)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
r0 = rf(ctx, cmd)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: ctx, cmd
func (_m *FakePublicDashboardStore) Update(ctx context.Context, cmd models.SavePublicDashboardCommand) error {
func (_m *FakePublicDashboardStore) Update(ctx context.Context, cmd models.SavePublicDashboardCommand) (int64, error) {
ret := _m.Called(ctx, cmd)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardCommand) int64); ok {
r0 = rf(ctx, cmd)
} else {
r0 = ret.Error(0)
r0 = ret.Get(0).(int64)
}
return r0
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardCommand) error); ok {
r1 = rf(ctx, cmd)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.

View File

@ -20,7 +20,8 @@ type Service interface {
FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error)
FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error)
Save(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error)
Delete(ctx context.Context, orgId int64, uid string) error
GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error)
@ -40,8 +41,8 @@ type Store interface {
FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error)
FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error)
FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error)
Save(ctx context.Context, cmd SavePublicDashboardCommand) error
Update(ctx context.Context, cmd SavePublicDashboardCommand) error
Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error)
Delete(ctx context.Context, orgId int64, uid string) (int64, error)
GetOrgIdByAccessToken(ctx context.Context, accessToken string) (int64, error)

View File

@ -399,7 +399,7 @@ func TestGetQueryDataResponse(t *testing.T) {
TimeSettings: timeSettings,
},
}
pubdashDto, err := service.Save(context.Background(), SignedInUser, dto)
pubdashDto, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
resp, _ := service.GetQueryDataResponse(context.Background(), true, publicDashboardQueryDTO, 1, pubdashDto.AccessToken)
@ -840,7 +840,7 @@ func TestBuildMetricRequest(t *testing.T) {
},
}
publicDashboardPD, err := service.Save(context.Background(), SignedInUser, dto)
publicDashboardPD, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
nonPublicDto := &SavePublicDashboardDTO{
@ -854,7 +854,7 @@ func TestBuildMetricRequest(t *testing.T) {
},
}
_, err = service.Save(context.Background(), SignedInUser, nonPublicDto)
_, err = service.Create(context.Background(), SignedInUser, nonPublicDto)
require.NoError(t, err)
t.Run("extracts queries from provided dashboard", func(t *testing.T) {

View File

@ -121,9 +121,76 @@ func (pd *PublicDashboardServiceImpl) FindByDashboardUid(ctx context.Context, or
return pubdash, nil
}
// Save is a helper method to persist the sharing config
// to the database. It handles validations for sharing config and persistence
func (pd *PublicDashboardServiceImpl) Save(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) {
// Creates and validates the public dashboard and saves it to the database
func (pd *PublicDashboardServiceImpl) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) {
// ensure dashboard exists
dashboard, err := pd.FindDashboard(ctx, u.OrgID, dto.DashboardUid)
if err != nil {
return nil, err
}
// set default value for time settings
if dto.PublicDashboard.TimeSettings == nil {
dto.PublicDashboard.TimeSettings = &TimeSettings{}
}
// validate fields
err = validation.ValidatePublicDashboard(dto, dashboard)
if err != nil {
return nil, err
}
// verify public dashboard does not exist and that we didn't get one from the
// request
existingPubdash, err := pd.store.Find(ctx, dto.PublicDashboard.Uid)
if err != nil {
return nil, err
} else if existingPubdash != nil {
return nil, ErrPublicDashboardBadRequest
}
uid, err := pd.NewPublicDashboardUid(ctx)
if err != nil {
return nil, err
}
accessToken, err := pd.NewPublicDashboardAccessToken(ctx)
if err != nil {
return nil, err
}
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: uid,
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
IsEnabled: dto.PublicDashboard.IsEnabled,
AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
CreatedBy: dto.UserId,
CreatedAt: time.Now(),
AccessToken: accessToken,
},
}
_, err = pd.store.Create(ctx, cmd)
if err != nil {
return nil, err
}
//Get latest public dashboard to return
newPubdash, err := pd.store.Find(ctx, uid)
if err != nil {
return nil, err
}
pd.logIsEnabledChanged(existingPubdash, newPubdash, u)
return newPubdash, err
}
// Updates an existing public dashboard based on publicdashboard.Uid
func (pd *PublicDashboardServiceImpl) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) {
// validate if the dashboard exists
dashboard, err := pd.FindDashboard(ctx, u.OrgID, dto.DashboardUid)
if err != nil {
@ -143,25 +210,41 @@ func (pd *PublicDashboardServiceImpl) Save(ctx context.Context, u *user.SignedIn
existingPubdash, err := pd.store.Find(ctx, dto.PublicDashboard.Uid)
if err != nil {
return nil, err
} else if existingPubdash == nil {
return nil, ErrPublicDashboardNotFound
}
// save changes
var pubdashUid string
if existingPubdash == nil {
err = validation.ValidateSavePublicDashboard(dto, dashboard)
if err != nil {
return nil, err
}
pubdashUid, err = pd.savePublicDashboard(ctx, dto)
} else {
pubdashUid, err = pd.updatePublicDashboard(ctx, dto)
}
// validate dashboard
err = validation.ValidatePublicDashboard(dto, dashboard)
if err != nil {
return nil, err
}
//Get latest public dashboard to return
newPubdash, err := pd.store.Find(ctx, pubdashUid)
// set values to update
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: existingPubdash.Uid,
IsEnabled: dto.PublicDashboard.IsEnabled,
AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
UpdatedBy: dto.UserId,
UpdatedAt: time.Now(),
},
}
// persist
affectedRows, err := pd.store.Update(ctx, cmd)
if err != nil {
return nil, err
}
// 404 if not found
if affectedRows == 0 {
return nil, ErrPublicDashboardNotFound
}
// get latest public dashboard to return
newPubdash, err := pd.store.Find(ctx, existingPubdash.Uid)
if err != nil {
return nil, err
}
@ -203,58 +286,6 @@ func (pd *PublicDashboardServiceImpl) NewPublicDashboardAccessToken(ctx context.
return "", ErrPublicDashboardFailedGenerateAccessToken
}
// Called by Save this handles business logic
// to generate token and calls create at the database layer
func (pd *PublicDashboardServiceImpl) savePublicDashboard(ctx context.Context, dto *SavePublicDashboardDTO) (string, error) {
uid, err := pd.NewPublicDashboardUid(ctx)
if err != nil {
return "", err
}
accessToken, err := pd.NewPublicDashboardAccessToken(ctx)
if err != nil {
return "", err
}
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: uid,
DashboardUid: dto.DashboardUid,
OrgId: dto.OrgId,
IsEnabled: dto.PublicDashboard.IsEnabled,
AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
CreatedBy: dto.UserId,
CreatedAt: time.Now(),
AccessToken: accessToken,
},
}
err = pd.store.Save(ctx, cmd)
if err != nil {
return "", err
}
return uid, nil
}
// Called by Save this handles business logic for updating a
// dashboard and calls update at the database layer
func (pd *PublicDashboardServiceImpl) updatePublicDashboard(ctx context.Context, dto *SavePublicDashboardDTO) (string, error) {
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: dto.PublicDashboard.Uid,
IsEnabled: dto.PublicDashboard.IsEnabled,
AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
UpdatedBy: dto.UserId,
UpdatedAt: time.Now(),
},
}
return dto.PublicDashboard.Uid, pd.store.Update(ctx, cmd)
}
// FindAll Returns a list of public dashboards by orgId
func (pd *PublicDashboardServiceImpl) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) {
publicDashboards, err := pd.store.FindAll(ctx, orgId)

View File

@ -120,8 +120,10 @@ func TestGetPublicDashboard(t *testing.T) {
}
}
func TestSavePublicDashboard(t *testing.T) {
t.Run("Saving public dashboard", func(t *testing.T) {
// We're using sqlite here because testing all of the behaviors with mocks in
// the correct order is convoluted.
func TestCreatePublicDashboard(t *testing.T) {
t.Run("Create public dashboard", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
publicdashboardStore := database.ProvideStore(sqlStore)
@ -145,7 +147,7 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
_, err := service.Save(context.Background(), SignedInUser, dto)
_, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid)
@ -189,7 +191,7 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
_, err := service.Save(context.Background(), SignedInUser, dto)
_, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid)
@ -220,11 +222,11 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
_, err := service.Save(context.Background(), SignedInUser, dto)
_, err := service.Create(context.Background(), SignedInUser, dto)
require.Error(t, err)
})
t.Run("Pubdash access token generation throws an error and pubdash is not persisted", func(t *testing.T) {
t.Run("Throws an error when pubdash with generated access token already exists", func(t *testing.T) {
dashboard := models.NewDashboard("testDashie")
pubdash := &PublicDashboard{
IsEnabled: true,
@ -238,7 +240,6 @@ func TestSavePublicDashboard(t *testing.T) {
publicDashboardStore.On("FindDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil)
publicDashboardStore.On("Find", mock.Anything, mock.Anything).Return(nil, nil)
publicDashboardStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(pubdash, nil)
publicDashboardStore.On("NewPublicDashboardUid", mock.Anything).Return("an-uid", nil)
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
@ -256,11 +257,59 @@ func TestSavePublicDashboard(t *testing.T) {
},
}
_, err := service.Save(context.Background(), SignedInUser, dto)
_, err := service.Create(context.Background(), SignedInUser, dto)
require.Error(t, err)
require.Equal(t, err, ErrPublicDashboardFailedGenerateAccessToken)
publicDashboardStore.AssertNotCalled(t, "Save")
publicDashboardStore.AssertNotCalled(t, "Create")
})
t.Run("Returns error if public dashboard exists", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
}
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 7,
PublicDashboard: &PublicDashboard{
AnnotationsEnabled: false,
IsEnabled: true,
TimeSettings: timeSettings,
},
}
savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// attempt to overwrite settings
dto = &SavePublicDashboardDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
UserId: 8,
PublicDashboard: &PublicDashboard{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
CreatedBy: 9,
CreatedAt: time.Time{},
IsEnabled: true,
AnnotationsEnabled: true,
TimeSettings: timeSettings,
AccessToken: "NOTAREALUUID",
},
}
_, err = service.Create(context.Background(), SignedInUser, dto)
assert.Equal(t, ErrPublicDashboardBadRequest, err)
})
}
@ -287,7 +336,8 @@ func TestUpdatePublicDashboard(t *testing.T) {
},
}
savedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
// insert initial pubdash
savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// attempt to overwrite settings
@ -308,10 +358,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
AccessToken: "NOTAREALUUID",
},
}
// Since the dto.PublicDashboard has a uid, this will call
// service.updatePublicDashboard
updatedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// don't get updated
@ -350,9 +397,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
},
}
// Since the dto.PublicDashboard has a uid, this will call
// service.updatePublicDashboard
savedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// attempt to overwrite settings
@ -372,7 +417,7 @@ func TestUpdatePublicDashboard(t *testing.T) {
},
}
updatedPubdash, err := service.Save(context.Background(), SignedInUser, dto)
updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto)
require.NoError(t, err)
assert.Equal(t, &TimeSettings{}, updatedPubdash.TimeSettings)
@ -422,81 +467,6 @@ func TestDeletePublicDashboard(t *testing.T) {
}
}
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *models.Dashboard {
t.Helper()
var dashboardPanels []interface{}
if customPanels != nil {
dashboardPanels = customPanels
} else {
dashboardPanels = []interface{}{
map[string]interface{}{
"id": 1,
"datasource": map[string]interface{}{
"uid": "ds1",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "ds1",
},
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "ds2",
},
"refId": "B",
},
},
},
map[string]interface{}{
"id": 2,
"datasource": map[string]interface{}{
"uid": "ds3",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "ds3",
},
"refId": "C",
},
},
},
}
}
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
"panels": dashboardPanels,
"templating": map[string]interface{}{
"list": templateVars,
},
"time": map[string]interface{}{
"from": "2022-09-01T00:00:00.000Z",
"to": "2022-09-01T12:00:00.000Z",
},
}),
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.Id)
dash.Data.Set("uid", dash.Uid)
return dash
}
func TestPublicDashboardServiceImpl_getSafeIntervalAndMaxDataPoints(t *testing.T) {
type args struct {
reqDTO PublicDashboardQueryDTO
@ -596,36 +566,6 @@ func TestDashboardEnabledChanged(t *testing.T) {
})
}
func CreateDatasource(dsType string, uid string) struct {
Type *string `json:"type,omitempty"`
Uid *string `json:"uid,omitempty"`
} {
return struct {
Type *string `json:"type,omitempty"`
Uid *string `json:"uid,omitempty"`
}{
Type: &dsType,
Uid: &uid,
}
}
func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard {
type annotationsDto struct {
List []DashAnnotation `json:"list"`
}
annos := annotationsDto{}
annos.List = annotations
annoJSON, err := json.Marshal(annos)
require.NoError(t, err)
dashAnnos, err := simplejson.NewJson(annoJSON)
require.NoError(t, err)
dash.Data.Set("annotations", dashAnnos)
return dash
}
func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
type args struct {
ctx context.Context
@ -962,3 +902,108 @@ func TestPublicDashboardServiceImpl_NewPublicDashboardAccessToken(t *testing.T)
})
}
}
func CreateDatasource(dsType string, uid string) struct {
Type *string `json:"type,omitempty"`
Uid *string `json:"uid,omitempty"`
} {
return struct {
Type *string `json:"type,omitempty"`
Uid *string `json:"uid,omitempty"`
}{
Type: &dsType,
Uid: &uid,
}
}
func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard {
type annotationsDto struct {
List []DashAnnotation `json:"list"`
}
annos := annotationsDto{}
annos.List = annotations
annoJSON, err := json.Marshal(annos)
require.NoError(t, err)
dashAnnos, err := simplejson.NewJson(annoJSON)
require.NoError(t, err)
dash.Data.Set("annotations", dashAnnos)
return dash
}
func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64,
folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *models.Dashboard {
t.Helper()
var dashboardPanels []interface{}
if customPanels != nil {
dashboardPanels = customPanels
} else {
dashboardPanels = []interface{}{
map[string]interface{}{
"id": 1,
"datasource": map[string]interface{}{
"uid": "ds1",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "ds1",
},
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "ds2",
},
"refId": "B",
},
},
},
map[string]interface{}{
"id": 2,
"datasource": map[string]interface{}{
"uid": "ds3",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "ds3",
},
"refId": "C",
},
},
},
}
}
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
"panels": dashboardPanels,
"templating": map[string]interface{}{
"list": templateVars,
},
"time": map[string]interface{}{
"from": "2022-09-01T00:00:00.000Z",
"to": "2022-09-01T12:00:00.000Z",
},
}),
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.Id)
dash.Data.Set("uid", dash.Uid)
return dash
}

View File

@ -7,7 +7,7 @@ import (
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
)
func ValidateSavePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error {
func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error {
if hasTemplateVariables(dashboard) {
return ErrPublicDashboardHasTemplateVariables
}

View File

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestValidateSavePublicDashboard(t *testing.T) {
func TestValidatePublicDashboard(t *testing.T) {
t.Run("Returns validation error when dashboard has template variables", func(t *testing.T) {
templateVars := []byte(`{
"templating": {
@ -24,7 +24,7 @@ func TestValidateSavePublicDashboard(t *testing.T) {
dashboard := models.NewDashboardFromJson(dashboardData)
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
err := ValidateSavePublicDashboard(dto, dashboard)
err := ValidatePublicDashboard(dto, dashboard)
require.ErrorContains(t, err, ErrPublicDashboardHasTemplateVariables.Reason)
})
@ -38,7 +38,7 @@ func TestValidateSavePublicDashboard(t *testing.T) {
dashboard := models.NewDashboardFromJson(dashboardData)
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil}
err := ValidateSavePublicDashboard(dto, dashboard)
err := ValidatePublicDashboard(dto, dashboard)
require.NoError(t, err)
})
}

View File

@ -35,10 +35,10 @@ const getConfigError = (err: { status: number }) => ({ error: err.status !== 404
export const publicDashboardApi = createApi({
reducerPath: 'publicDashboardApi',
baseQuery: retry(backendSrvBaseQuery({ baseUrl: '/api/dashboards' }), { maxRetries: 0 }),
tagTypes: ['Config', 'PublicDashboards'],
tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard'],
keepUnusedDataFor: 0,
endpoints: (builder) => ({
getConfig: builder.query<PublicDashboard, string>({
getPublicDashboard: builder.query<PublicDashboard, string>({
query: (dashboardUid) => ({
url: `/uid/${dashboardUid}/public-dashboards`,
manageError: getConfigError,
@ -53,9 +53,9 @@ export const publicDashboardApi = createApi({
dispatch(notifyApp(createErrorNotification(customError?.error?.data?.message)));
}
},
providesTags: ['Config'],
providesTags: ['PublicDashboard'],
}),
saveConfig: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
createPublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
query: (params) => ({
url: `/uid/${params.dashboard.uid}/public-dashboards`,
method: 'POST',
@ -63,21 +63,42 @@ export const publicDashboardApi = createApi({
}),
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled;
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
dispatch(notifyApp(createSuccessNotification('Public dashboard created!')));
// Update runtime meta flag
dashboard.updateMeta({
hasPublicDashboard: true,
publicDashboardUid: data.uid,
publicDashboardEnabled: data.isEnabled,
});
},
invalidatesTags: ['Config'],
invalidatesTags: ['PublicDashboard'],
}),
updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
query: (params) => ({
url: `/uid/${params.dashboard.uid}/public-dashboards/${params.payload.uid}`,
method: 'PUT',
data: params.payload,
}),
extraOptions: { maxRetries: 0 },
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled;
dispatch(notifyApp(createSuccessNotification('Public dashboard updated!')));
// Update runtime meta flag
dashboard.updateMeta({
hasPublicDashboard: true,
publicDashboardUid: data.uid,
publicDashboardEnabled: data.isEnabled,
});
},
invalidatesTags: ['PublicDashboard'],
}),
listPublicDashboards: builder.query<ListPublicDashboardResponse[], void>({
query: () => ({
url: '/public-dashboards',
}),
providesTags: ['PublicDashboards'],
providesTags: ['AuditTablePublicDashboard'],
}),
deletePublicDashboard: builder.mutation<void, { dashboardTitle: string; dashboardUid: string; uid: string }>({
query: (params) => ({
@ -97,14 +118,15 @@ export const publicDashboardApi = createApi({
)
);
},
invalidatesTags: ['PublicDashboards'],
invalidatesTags: ['AuditTablePublicDashboard'],
}),
}),
});
export const {
useGetConfigQuery,
useSaveConfigMutation,
useGetPublicDashboardQuery,
useCreatePublicDashboardMutation,
useUpdatePublicDashboardMutation,
useDeletePublicDashboardMutation,
useListPublicDashboardsQuery,
} = publicDashboardApi;

View File

@ -17,20 +17,7 @@ import { configureStore } from 'app/store/configureStore';
import { ShareModal } from '../ShareModal';
const server = setupServer(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (_, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
isEnabled: false,
annotationsEnabled: false,
uid: undefined,
dashboardUid: undefined,
accessToken: 'an-access-token',
})
);
})
);
const server = setupServer();
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
@ -147,6 +134,7 @@ describe('SharePublic', () => {
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 01:59:59')).toBeInTheDocument();
});
it('when modal is opened, then loader spinner appears and inputs are disabled', async () => {
mockDashboard.meta.hasPublicDashboard = true;
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
@ -158,6 +146,7 @@ describe('SharePublic', () => {
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
});
it('when fetch errors happen, then all inputs remain disabled', async () => {
mockDashboard.meta.hasPublicDashboard = true;
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(ctx.status(500));
@ -165,7 +154,7 @@ describe('SharePublic', () => {
);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner'), { timeout: 7000 });
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
@ -178,13 +167,16 @@ describe('SharePublic', () => {
});
describe('SharePublic - New config setup', () => {
beforeEach(() => {
mockDashboard.meta.hasPublicDashboard = false;
});
it('when modal is opened, then save button is disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
});
it('when fetch is done, then loader spinner is gone, inputs are enabled and save button is disabled', async () => {
it('when fetch is done, then no loader spinner appears, inputs are enabled and save button is disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeEnabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeEnabled();
@ -196,7 +188,7 @@ describe('SharePublic - New config setup', () => {
});
it('when checkboxes are filled, then save button remains disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
@ -206,7 +198,7 @@ describe('SharePublic - New config setup', () => {
});
it('when checkboxes and switch are filled, then save button is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
@ -219,6 +211,7 @@ describe('SharePublic - New config setup', () => {
describe('SharePublic - Already persisted', () => {
beforeEach(() => {
mockDashboard.meta.hasPublicDashboard = true;
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(

View File

@ -6,7 +6,11 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src';
import { Alert, Button, ClipboardButton, Field, HorizontalGroup, Input, useStyles2, Spinner } from '@grafana/ui/src';
import { contextSrv } from 'app/core/services/context_srv';
import { useGetConfigQuery, useSaveConfigMutation } from 'app/features/dashboard/api/publicDashboardApi';
import {
useGetPublicDashboardQuery,
useCreatePublicDashboardMutation,
useUpdatePublicDashboardMutation,
} from 'app/features/dashboard/api/publicDashboardApi';
import { AcknowledgeCheckboxes } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes';
import { Configuration } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration';
import { Description } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Description';
@ -27,13 +31,19 @@ export const SharePublicDashboard = (props: Props) => {
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const styles = useStyles2(getStyles);
const [hasPublicDashboard, setHasPublicDashboard] = useState(props.dashboard.meta.hasPublicDashboard);
const {
isLoading: isFetchingLoading,
data: publicDashboard,
isError: isFetchingError,
} = useGetConfigQuery(props.dashboard.uid);
} = useGetPublicDashboardQuery(props.dashboard.uid, {
// if we don't have a public dashboard, don't try to load public dashboard
skip: !hasPublicDashboard,
});
const [saveConfig, { isLoading: isSaveLoading }] = useSaveConfigMutation();
const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
const [updatePublicDashboard, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
const [acknowledgements, setAcknowledgements] = useState<Acknowledgements>({
public: false,
@ -63,7 +73,7 @@ export const SharePublicDashboard = (props: Props) => {
setEnabledSwitch((prevState) => ({ ...prevState, isEnabled: !!publicDashboard?.isEnabled }));
}, [publicDashboard]);
const isLoading = isFetchingLoading || isSaveLoading;
const isLoading = isFetchingLoading || isSaveLoading || isUpdateLoading;
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
const isSaveEnabled = useMemo(
@ -77,13 +87,23 @@ export const SharePublicDashboard = (props: Props) => {
[hasWritePermissions, acknowledged, props.dashboard, isLoading, isFetchingError, enabledSwitch, publicDashboard]
);
const onSavePublicConfig = () => {
const onSavePublicConfig = async () => {
reportInteraction('grafana_dashboards_public_create_clicked');
saveConfig({
const req = {
dashboard: props.dashboard,
payload: { ...publicDashboard!, isEnabled: enabledSwitch.isEnabled, annotationsEnabled },
});
};
// create or update based on whether we have existing uid
if (hasPublicDashboard) {
await updatePublicDashboard(req).unwrap();
setHasPublicDashboard(true);
} else {
await createPublicDashboard(req).unwrap();
setHasPublicDashboard(true);
}
};
const onAcknowledge = (field: string, checked: boolean) => {

View File

@ -44,6 +44,7 @@ export interface DashboardMeta {
publicDashboardAccessToken?: string;
publicDashboardUid?: string;
publicDashboardEnabled?: boolean;
hasPublicDashboard?: boolean;
dashboardNotFound?: boolean;
}