PublicDashboards: Audit table redesign (#68137)

pull/68664/head
Juan Cabanas 2 years ago committed by GitHub
parent 74e87ccbbd
commit 9890ff7c92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      e2e/dashboards-suite/dashboard-public-create.spec.ts
  2. 1
      packages/grafana-e2e-selectors/src/selectors/pages.ts
  3. 26
      pkg/services/publicdashboards/api/api.go
  4. 2
      pkg/services/publicdashboards/api/api_test.go
  5. 7
      pkg/services/publicdashboards/api/query_test.go
  6. 2
      pkg/services/publicdashboards/database/database.go
  7. 8
      pkg/services/publicdashboards/database/database_test.go
  8. 40
      pkg/services/publicdashboards/models/models.go
  9. 10
      pkg/services/publicdashboards/public_dashboard_service_mock.go
  10. 3
      pkg/services/publicdashboards/public_dashboard_store_mock.go
  11. 20
      pkg/services/publicdashboards/service/query_test.go
  12. 136
      pkg/services/publicdashboards/service/service.go
  13. 311
      pkg/services/publicdashboards/service/service_test.go
  14. 4
      pkg/services/publicdashboards/validation/validation_test.go
  15. 23
      public/app/features/dashboard/api/publicDashboardApi.ts
  16. 2
      public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx
  17. 8
      public/app/features/manage-dashboards/PublicDashboardListPage.tsx
  18. 2
      public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx
  19. 74
      public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.test.tsx
  20. 278
      public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx

@ -141,7 +141,7 @@ e2e.scenario({
.then((text) => e2e().wrap(text).as('url'));
// Save public dashboard
e2e().intercept('PUT', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
e2e().intercept('PATCH', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
// Switch off enabling toggle
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('be.enabled').click({ force: true });
e2e().wait('@update');

@ -286,6 +286,7 @@ export const Pages = {
linkButton: 'public-dashboard-link-button',
configButton: 'public-dashboard-configuration-button',
trashcanButton: 'public-dashboard-remove-button',
pauseSwitch: 'data-testid public dashboard pause switch',
},
},
UserListPage: {

@ -77,7 +77,7 @@ func (api *Api) RegisterAPIEndpoints() {
routing.Wrap(api.CreatePublicDashboard))
// Update Public Dashboard
api.RouteRegister.Put("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
api.RouteRegister.Patch("/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
auth(accesscontrol.EvalPermission(dashboards.ActionDashboardsPublicWrite, uidScope)),
routing.Wrap(api.UpdatePublicDashboard))
@ -127,22 +127,21 @@ func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Respo
return response.Err(ErrInvalidUid.Errorf("CreatePublicDashboard: invalid Uid %s", dashboardUid))
}
pd := &PublicDashboard{}
if err := web.Bind(c.Req, pd); err != nil {
pdDTO := &PublicDashboardDTO{}
if err := web.Bind(c.Req, pdDTO); err != nil {
return response.Err(ErrBadRequest.Errorf("CreatePublicDashboard: bad request data %v", err))
}
// Always set the orgID and userID from the session
pd.OrgId = c.OrgID
dto := SavePublicDashboardDTO{
pdDTO.OrgId = c.OrgID
dto := &SavePublicDashboardDTO{
UserId: c.UserID,
OrgId: c.OrgID,
DashboardUid: dashboardUid,
PublicDashboard: pd,
PublicDashboard: pdDTO,
}
//Create the public dashboard
pd, err := api.PublicDashboardService.Create(c.Req.Context(), c.SignedInUser, &dto)
pd, err := api.PublicDashboardService.Create(c.Req.Context(), c.SignedInUser, dto)
if err != nil {
return response.Err(err)
}
@ -164,19 +163,18 @@ func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Respo
return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid))
}
pd := &PublicDashboard{}
if err := web.Bind(c.Req, pd); err != nil {
pdDTO := &PublicDashboardDTO{}
if err := web.Bind(c.Req, pdDTO); err != nil {
return response.Err(ErrBadRequest.Errorf("UpdatePublicDashboard: bad request data %v", err))
}
// Always set the orgID and userID from the session
pd.OrgId = c.OrgID
pd.Uid = uid
pdDTO.OrgId = c.OrgID
pdDTO.Uid = uid
dto := SavePublicDashboardDTO{
UserId: c.UserID,
OrgId: c.OrgID,
DashboardUid: dashboardUid,
PublicDashboard: pd,
PublicDashboard: pdDTO,
}
// Update the public dashboard

@ -572,7 +572,7 @@ func TestAPIUpdatePublicDashboard(t *testing.T) {
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)
response := callAPI(testServer, http.MethodPatch, url, body, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
// check whether service called

@ -310,11 +310,12 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
require.NoError(t, err)
// Create public dashboard
isEnabled := true
savePubDashboardCmd := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
OrgId: dashboard.OrgID,
},
}

@ -42,7 +42,7 @@ func (d *PublicDashboardStoreImpl) FindAll(ctx context.Context, orgId int64) ([]
"dashboard_public.uid, dashboard_public.access_token, dashboard.uid as dashboard_uid, dashboard_public.is_enabled, dashboard.title").
Join("LEFT", "dashboard", "dashboard.uid = dashboard_public.dashboard_uid AND dashboard.org_id = dashboard_public.org_id").
Where("dashboard_public.org_id = ?", orgId).
OrderBy(" is_enabled DESC, dashboard.title IS NULL, dashboard.title ASC")
OrderBy(" dashboard.title IS NULL, dashboard.title ASC")
err := sess.Find(&resp)
return err

@ -42,14 +42,14 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
var orgId int64 = 1
aDash := insertTestDashboard(t, dashboardStore, "a", orgId, 0, true)
bDash := insertTestDashboard(t, dashboardStore, "b", orgId, 0, true)
aDash := insertTestDashboard(t, dashboardStore, "a", orgId, 0, true)
cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true)
// these are in order of how they should be returned from ListPUblicDashboards
a := insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType)
b := insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true, PublicShareType)
c := insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType)
a := insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false, PublicShareType)
b := insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true, PublicShareType)
c := insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true, PublicShareType)
// this is case that can happen as of now, however, postgres and mysql sort
// null in the exact opposite fashion and there is no shared syntax to sort

@ -40,20 +40,39 @@ var (
type ShareType string
type PublicDashboard struct {
Uid string `json:"uid" xorm:"pk uid"`
DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"`
OrgId int64 `json:"-" xorm:"org_id"` // Don't ever marshal orgId to Json
Uid string `json:"uid" xorm:"pk uid"`
DashboardUid string `json:"dashboardUid" xorm:"dashboard_uid"`
OrgId int64 `json:"-" xorm:"org_id"` // Don't ever marshal orgId to Json
AccessToken string `json:"accessToken" xorm:"access_token"`
CreatedBy int64 `json:"createdBy" xorm:"created_by"`
UpdatedBy int64 `json:"updatedBy" xorm:"updated_by"`
CreatedAt time.Time `json:"createdAt" xorm:"created_at"`
UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at"`
//config fields
TimeSettings *TimeSettings `json:"timeSettings" xorm:"time_settings"`
TimeSelectionEnabled bool `json:"timeSelectionEnabled" xorm:"time_selection_enabled"`
IsEnabled bool `json:"isEnabled" xorm:"is_enabled"`
AccessToken string `json:"accessToken" xorm:"access_token"`
AnnotationsEnabled bool `json:"annotationsEnabled" xorm:"annotations_enabled"`
TimeSelectionEnabled bool `json:"timeSelectionEnabled" xorm:"time_selection_enabled"`
Share ShareType `json:"share" xorm:"share"`
Recipients []EmailDTO `json:"recipients,omitempty" xorm:"-"`
CreatedBy int64 `json:"createdBy" xorm:"created_by"`
UpdatedBy int64 `json:"updatedBy" xorm:"updated_by"`
CreatedAt time.Time `json:"createdAt" xorm:"created_at"`
UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at"`
}
type PublicDashboardDTO struct {
Uid string `json:"uid"`
DashboardUid string `json:"dashboardUid"`
OrgId int64 `json:"-"` // Don't ever marshal orgId to Json
AccessToken string `json:"accessToken"`
CreatedBy int64 `json:"createdBy"`
UpdatedBy int64 `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
//config fields
TimeSettings *TimeSettings `json:"timeSettings"`
TimeSelectionEnabled *bool `json:"timeSelectionEnabled"`
IsEnabled *bool `json:"isEnabled"`
AnnotationsEnabled *bool `json:"annotationsEnabled"`
Share ShareType `json:"share"`
Recipients []EmailDTO `json:"recipients,omitempty"`
}
type EmailDTO struct {
@ -130,9 +149,8 @@ func (pd PublicDashboard) BuildTimeSettings(dashboard *dashboards.Dashboard, req
// DTO for transforming user input in the api
type SavePublicDashboardDTO struct {
DashboardUid string
OrgId int64
UserId int64
PublicDashboard *PublicDashboard
PublicDashboard *PublicDashboardDTO
}
type PublicDashboardQueryDTO struct {

@ -360,13 +360,13 @@ func (_m *FakePublicDashboardService) GetOrgIdByAccessToken(ctx context.Context,
return r0, r1
}
// GetQueryDataResponse provides a mock function with given fields: ctx, skipCache, reqDTO, panelId, accessToken
func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO models.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) {
ret := _m.Called(ctx, skipCache, reqDTO, panelId, accessToken)
// GetQueryDataResponse provides a mock function with given fields: ctx, skipDSCache, reqDTO, panelId, accessToken
func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, skipDSCache bool, reqDTO models.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) {
ret := _m.Called(ctx, skipDSCache, reqDTO, panelId, accessToken)
var r0 *backend.QueryDataResponse
if rf, ok := ret.Get(0).(func(context.Context, bool, models.PublicDashboardQueryDTO, int64, string) *backend.QueryDataResponse); ok {
r0 = rf(ctx, skipCache, reqDTO, panelId, accessToken)
r0 = rf(ctx, skipDSCache, reqDTO, panelId, accessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*backend.QueryDataResponse)
@ -375,7 +375,7 @@ func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context,
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, bool, models.PublicDashboardQueryDTO, int64, string) error); ok {
r1 = rf(ctx, skipCache, reqDTO, panelId, accessToken)
r1 = rf(ctx, skipDSCache, reqDTO, panelId, accessToken)
} else {
r1 = ret.Error(1)
}

@ -6,8 +6,9 @@ import (
context "context"
dashboards "github.com/grafana/grafana/pkg/services/dashboards"
models "github.com/grafana/grafana/pkg/services/publicdashboards/models"
mock "github.com/stretchr/testify/mock"
models "github.com/grafana/grafana/pkg/services/publicdashboards/models"
)
// FakePublicDashboardStore is an autogenerated mock type for the Store type

@ -688,14 +688,14 @@ func TestGetQueryDataResponse(t *testing.T) {
}}
dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels)
isEnabled := true
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
OrgId: dashboard.OrgID,
TimeSettings: timeSettings,
},
}
@ -1199,11 +1199,11 @@ func TestBuildMetricRequest(t *testing.T) {
MaxDataPoints: int64(200),
}
isEnabled := true
dto := &SavePublicDashboardDTO{
DashboardUid: publicDashboard.UID,
OrgId: publicDashboard.OrgID,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
TimeSettings: timeSettings,
@ -1213,11 +1213,11 @@ func TestBuildMetricRequest(t *testing.T) {
publicDashboardPD, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
isEnabled = false
nonPublicDto := &SavePublicDashboardDTO{
DashboardUid: nonPublicDashboard.UID,
OrgId: nonPublicDashboard.OrgID,
PublicDashboard: &PublicDashboard{
IsEnabled: false,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
TimeSettings: defaultPubdashTimeSettings,

@ -162,50 +162,24 @@ func (pd *PublicDashboardServiceImpl) Create(ctx context.Context, u *user.Signed
return nil, ErrDashboardIsPublic.Errorf("Create: public dashboard for dashboard %s already exists", dto.DashboardUid)
}
// set default value for time settings
if dto.PublicDashboard.TimeSettings == nil {
dto.PublicDashboard.TimeSettings = &TimeSettings{}
}
if dto.PublicDashboard.Share == "" {
dto.PublicDashboard.Share = PublicShareType
}
uid, err := pd.NewPublicDashboardUid(ctx)
if err != nil {
return nil, err
}
accessToken, err := pd.NewPublicDashboardAccessToken(ctx)
publicDashboard, err := pd.newCreatePublicDashboard(ctx, dto)
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,
TimeSelectionEnabled: dto.PublicDashboard.TimeSelectionEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
Share: dto.PublicDashboard.Share,
CreatedBy: dto.UserId,
CreatedAt: time.Now(),
AccessToken: accessToken,
},
PublicDashboard: *publicDashboard,
}
affectedRows, err := pd.store.Create(ctx, cmd)
if err != nil {
return nil, ErrInternalServerError.Errorf("Create: failed to create the public dashboard with Uid %s: %w", uid, err)
return nil, ErrInternalServerError.Errorf("Create: failed to create the public dashboard with Uid %s: %w", publicDashboard.Uid, err)
} else if affectedRows == 0 {
return nil, ErrInternalServerError.Errorf("Create: failed to create a database entry for public dashboard with Uid %s. 0 rows changed, no error reported.", uid)
return nil, ErrInternalServerError.Errorf("Create: failed to create a database entry for public dashboard with Uid %s. 0 rows changed, no error reported.", publicDashboard.Uid)
}
//Get latest public dashboard to return
newPubdash, err := pd.store.Find(ctx, uid)
newPubdash, err := pd.store.Find(ctx, publicDashboard.Uid)
if err != nil {
return nil, ErrInternalServerError.Errorf("Create: failed to find the public dashboard: %w", err)
}
@ -241,27 +215,11 @@ func (pd *PublicDashboardServiceImpl) Update(ctx context.Context, u *user.Signed
return nil, ErrPublicDashboardNotFound.Errorf("Update: public dashboard not found by uid: %s", dto.PublicDashboard.Uid)
}
// set default value for time settings
if dto.PublicDashboard.TimeSettings == nil {
dto.PublicDashboard.TimeSettings = &TimeSettings{}
}
if dto.PublicDashboard.Share == "" {
dto.PublicDashboard.Share = existingPubdash.Share
}
publicDashboard := newUpdatePublicDashboard(dto, existingPubdash)
// set values to update
cmd := SavePublicDashboardCommand{
PublicDashboard: PublicDashboard{
Uid: existingPubdash.Uid,
IsEnabled: dto.PublicDashboard.IsEnabled,
AnnotationsEnabled: dto.PublicDashboard.AnnotationsEnabled,
TimeSelectionEnabled: dto.PublicDashboard.TimeSelectionEnabled,
TimeSettings: dto.PublicDashboard.TimeSettings,
Share: dto.PublicDashboard.Share,
UpdatedBy: dto.UserId,
UpdatedAt: time.Now(),
},
PublicDashboard: *publicDashboard,
}
// persist
@ -449,3 +407,83 @@ func GenerateAccessToken() (string, error) {
}
return fmt.Sprintf("%x", token[:]), nil
}
func (pd *PublicDashboardServiceImpl) newCreatePublicDashboard(ctx context.Context, dto *SavePublicDashboardDTO) (*PublicDashboard, error) {
uid, err := pd.NewPublicDashboardUid(ctx)
if err != nil {
return nil, err
}
accessToken, err := pd.NewPublicDashboardAccessToken(ctx)
if err != nil {
return nil, err
}
isEnabled := returnValueOrDefault(dto.PublicDashboard.IsEnabled, false)
annotationsEnabled := returnValueOrDefault(dto.PublicDashboard.AnnotationsEnabled, false)
timeSelectionEnabled := returnValueOrDefault(dto.PublicDashboard.TimeSelectionEnabled, false)
timeSettings := dto.PublicDashboard.TimeSettings
if dto.PublicDashboard.TimeSettings == nil {
timeSettings = &TimeSettings{}
}
share := dto.PublicDashboard.Share
if dto.PublicDashboard.Share == "" {
share = PublicShareType
}
return &PublicDashboard{
Uid: uid,
DashboardUid: dto.DashboardUid,
OrgId: dto.PublicDashboard.OrgId,
IsEnabled: isEnabled,
AnnotationsEnabled: annotationsEnabled,
TimeSelectionEnabled: timeSelectionEnabled,
TimeSettings: timeSettings,
Share: share,
CreatedBy: dto.UserId,
CreatedAt: time.Now(),
AccessToken: accessToken,
}, nil
}
func newUpdatePublicDashboard(dto *SavePublicDashboardDTO, pd *PublicDashboard) *PublicDashboard {
pubdashDTO := dto.PublicDashboard
timeSelectionEnabled := returnValueOrDefault(pubdashDTO.TimeSelectionEnabled, pd.TimeSelectionEnabled)
isEnabled := returnValueOrDefault(pubdashDTO.IsEnabled, pd.IsEnabled)
annotationsEnabled := returnValueOrDefault(pubdashDTO.AnnotationsEnabled, pd.AnnotationsEnabled)
timeSettings := pubdashDTO.TimeSettings
if pubdashDTO.TimeSettings == nil {
if pd.TimeSettings == nil {
timeSettings = &TimeSettings{}
} else {
timeSettings = pd.TimeSettings
}
}
share := pubdashDTO.Share
if pubdashDTO.Share == "" {
share = pd.Share
}
return &PublicDashboard{
Uid: pd.Uid,
IsEnabled: isEnabled,
AnnotationsEnabled: annotationsEnabled,
TimeSelectionEnabled: timeSelectionEnabled,
TimeSettings: timeSettings,
Share: share,
UpdatedBy: dto.UserId,
UpdatedAt: time.Now(),
}
}
func returnValueOrDefault(value *bool, defaultValue bool) bool {
if value != nil {
return *value
}
return defaultValue
}

@ -205,17 +205,18 @@ func TestCreatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled, annotationsEnabled, timeSelectionEnabled := true, false, true
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
AnnotationsEnabled: false,
TimeSelectionEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
AnnotationsEnabled: &annotationsEnabled,
TimeSelectionEnabled: &timeSelectionEnabled,
Share: EmailShareType,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
OrgId: dashboard.OrgID,
TimeSettings: timeSettings,
},
}
@ -230,10 +231,10 @@ func TestCreatePublicDashboard(t *testing.T) {
assert.Equal(t, dashboard.UID, pubdash.DashboardUid)
assert.Equal(t, dashboard.OrgID, pubdash.OrgId)
assert.Equal(t, dto.UserId, pubdash.CreatedBy)
assert.Equal(t, dto.PublicDashboard.AnnotationsEnabled, pubdash.AnnotationsEnabled)
assert.Equal(t, dto.PublicDashboard.TimeSelectionEnabled, pubdash.TimeSelectionEnabled)
assert.Equal(t, *dto.PublicDashboard.AnnotationsEnabled, pubdash.AnnotationsEnabled)
assert.Equal(t, *dto.PublicDashboard.TimeSelectionEnabled, pubdash.TimeSelectionEnabled)
// ExistsEnabledByDashboardUid set by parameters
assert.Equal(t, dto.PublicDashboard.IsEnabled, pubdash.IsEnabled)
assert.Equal(t, *dto.PublicDashboard.IsEnabled, pubdash.IsEnabled)
// CreatedAt set to non-zero time
assert.NotEqual(t, &time.Time{}, pubdash.CreatedAt)
// Time settings set by db
@ -244,6 +245,81 @@ func TestCreatePublicDashboard(t *testing.T) {
require.NoError(t, err, "expected a valid UUID, got %s", pubdash.AccessToken)
})
trueBooleanField := true
testCases := []struct {
Name string
IsEnabled *bool
TimeSelectionEnabled *bool
AnnotationsEnabled *bool
}{
{
Name: "isEnabled",
IsEnabled: nil,
TimeSelectionEnabled: &trueBooleanField,
AnnotationsEnabled: &trueBooleanField,
},
{
Name: "timeSelectionEnabled",
IsEnabled: &trueBooleanField,
TimeSelectionEnabled: nil,
AnnotationsEnabled: &trueBooleanField,
},
{
Name: "annotationsEnabled",
IsEnabled: &trueBooleanField,
TimeSelectionEnabled: &trueBooleanField,
AnnotationsEnabled: nil,
},
{
Name: "isEnabled, timeSelectionEnabled and annotationsEnabled",
IsEnabled: nil,
TimeSelectionEnabled: nil,
AnnotationsEnabled: nil,
},
}
for _, tt := range testCases {
t.Run(fmt.Sprintf("Create public dashboard with %s null boolean fields stores them as false", tt.Name), func(t *testing.T) {
sqlStore := db.InitTestDB(t)
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
serviceWrapper: serviceWrapper,
}
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
UserId: 7,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: tt.IsEnabled,
TimeSelectionEnabled: tt.TimeSelectionEnabled,
AnnotationsEnabled: tt.AnnotationsEnabled,
Share: PublicShareType,
DashboardUid: "NOTTHESAME",
OrgId: dashboard.OrgID,
TimeSettings: timeSettings,
},
}
_, err = service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID)
require.NoError(t, err)
assertFalseIfNull(t, pubdash.IsEnabled, dto.PublicDashboard.IsEnabled)
assertFalseIfNull(t, pubdash.TimeSelectionEnabled, dto.PublicDashboard.TimeSelectionEnabled)
assertFalseIfNull(t, pubdash.AnnotationsEnabled, dto.PublicDashboard.AnnotationsEnabled)
})
}
t.Run("Validate pubdash has default time setting value", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
quotaService := quotatest.New(false, nil)
@ -259,14 +335,14 @@ func TestCreatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled := true
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
OrgId: dashboard.OrgID,
},
}
@ -294,14 +370,14 @@ func TestCreatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled := true
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
OrgId: dashboard.OrgID,
},
}
@ -321,7 +397,7 @@ func TestCreatePublicDashboard(t *testing.T) {
IsEnabled: true,
AnnotationsEnabled: false,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
OrgId: dashboard.OrgID,
TimeSettings: timeSettings,
}
@ -339,14 +415,14 @@ func TestCreatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled := true
dto := &SavePublicDashboardDTO{
DashboardUid: "an-id",
OrgId: 8,
UserId: 7,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
OrgId: dashboard.OrgID,
},
}
@ -376,13 +452,13 @@ func TestCreatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled, annotationsEnabled := true, false
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
AnnotationsEnabled: false,
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
AnnotationsEnabled: &annotationsEnabled,
IsEnabled: &isEnabled,
TimeSettings: timeSettings,
},
}
@ -408,14 +484,14 @@ func TestCreatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled := true
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
DashboardUid: "NOTTHESAME",
OrgId: 9999999,
OrgId: dashboard.OrgID,
},
}
@ -429,6 +505,14 @@ func TestCreatePublicDashboard(t *testing.T) {
})
}
func assertFalseIfNull(t *testing.T, expectedValue bool, nullableValue *bool) {
if nullableValue == nil {
assert.Equal(t, expectedValue, false)
} else {
assert.Equal(t, expectedValue, *nullableValue)
}
}
func TestUpdatePublicDashboard(t *testing.T) {
t.Run("Updating public dashboard", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
@ -445,14 +529,14 @@ func TestUpdatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled, annotationsEnabled, timeSelectionEnabled := true, false, false
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
AnnotationsEnabled: false,
IsEnabled: true,
TimeSelectionEnabled: false,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
AnnotationsEnabled: &annotationsEnabled,
TimeSelectionEnabled: &timeSelectionEnabled,
TimeSettings: timeSettings,
},
}
@ -461,21 +545,21 @@ func TestUpdatePublicDashboard(t *testing.T) {
savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
isEnabled, annotationsEnabled, timeSelectionEnabled = true, true, true
// attempt to overwrite settings
dto = &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 8,
PublicDashboard: &PublicDashboard{
PublicDashboard: &PublicDashboardDTO{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
CreatedBy: 9,
CreatedAt: time.Time{},
IsEnabled: true,
AnnotationsEnabled: true,
TimeSelectionEnabled: true,
IsEnabled: &isEnabled,
AnnotationsEnabled: &annotationsEnabled,
TimeSelectionEnabled: &timeSelectionEnabled,
TimeSettings: timeSettings,
AccessToken: "NOTAREALUUID",
},
@ -491,9 +575,9 @@ func TestUpdatePublicDashboard(t *testing.T) {
assert.Equal(t, savedPubdash.AccessToken, updatedPubdash.AccessToken)
// gets updated
assert.Equal(t, dto.PublicDashboard.IsEnabled, updatedPubdash.IsEnabled)
assert.Equal(t, dto.PublicDashboard.AnnotationsEnabled, updatedPubdash.AnnotationsEnabled)
assert.Equal(t, dto.PublicDashboard.TimeSelectionEnabled, updatedPubdash.TimeSelectionEnabled)
assert.Equal(t, *dto.PublicDashboard.IsEnabled, updatedPubdash.IsEnabled)
assert.Equal(t, *dto.PublicDashboard.AnnotationsEnabled, updatedPubdash.AnnotationsEnabled)
assert.Equal(t, *dto.PublicDashboard.TimeSelectionEnabled, updatedPubdash.TimeSelectionEnabled)
assert.Equal(t, dto.PublicDashboard.TimeSettings, updatedPubdash.TimeSettings)
assert.Equal(t, dto.UserId, updatedPubdash.UpdatedBy)
assert.NotEqual(t, &time.Time{}, updatedPubdash.UpdatedAt)
@ -515,12 +599,12 @@ func TestUpdatePublicDashboard(t *testing.T) {
serviceWrapper: serviceWrapper,
}
isEnabled := true
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 7,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
TimeSettings: timeSettings,
},
}
@ -531,17 +615,16 @@ func TestUpdatePublicDashboard(t *testing.T) {
// attempt to overwrite settings
dto = &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
OrgId: dashboard.OrgID,
UserId: 8,
PublicDashboard: &PublicDashboard{
PublicDashboard: &PublicDashboardDTO{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
CreatedBy: 9,
CreatedAt: time.Time{},
IsEnabled: true,
AccessToken: "NOTAREALUUID",
IsEnabled: &isEnabled,
TimeSettings: &TimeSettings{},
AccessToken: "NOTAREALUUID",
},
}
@ -550,6 +633,132 @@ func TestUpdatePublicDashboard(t *testing.T) {
assert.Equal(t, &TimeSettings{}, updatedPubdash.TimeSettings)
})
trueBooleanField := true
timeSettings := &TimeSettings{From: "now-8", To: "now"}
shareType := EmailShareType
testCases := []struct {
Name string
IsEnabled *bool
TimeSelectionEnabled *bool
AnnotationsEnabled *bool
TimeSettings *TimeSettings
ShareType ShareType
}{
{
Name: "isEnabled",
IsEnabled: nil,
TimeSelectionEnabled: &trueBooleanField,
AnnotationsEnabled: &trueBooleanField,
TimeSettings: timeSettings,
ShareType: shareType,
},
{
Name: "timeSelectionEnabled",
IsEnabled: &trueBooleanField,
TimeSelectionEnabled: nil,
AnnotationsEnabled: &trueBooleanField,
TimeSettings: timeSettings,
ShareType: shareType,
},
{
Name: "annotationsEnabled",
IsEnabled: &trueBooleanField,
TimeSelectionEnabled: &trueBooleanField,
AnnotationsEnabled: nil,
TimeSettings: timeSettings,
ShareType: shareType,
},
{
Name: "isEnabled, timeSelectionEnabled and annotationsEnabled",
IsEnabled: nil,
TimeSelectionEnabled: nil,
AnnotationsEnabled: nil,
TimeSettings: nil,
ShareType: "",
},
}
for _, tt := range testCases {
t.Run(fmt.Sprintf("Update public dashboard with %s null boolean fields let those fields with old persisted value", tt.Name), func(t *testing.T) {
sqlStore := db.InitTestDB(t)
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
serviceWrapper := ProvideServiceWrapper(publicdashboardStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
store: publicdashboardStore,
serviceWrapper: serviceWrapper,
}
isEnabled, annotationsEnabled, timeSelectionEnabled := true, true, false
dto := &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
UserId: 7,
PublicDashboard: &PublicDashboardDTO{
IsEnabled: &isEnabled,
AnnotationsEnabled: &annotationsEnabled,
TimeSelectionEnabled: &timeSelectionEnabled,
TimeSettings: timeSettings,
Share: PublicShareType,
},
}
// insert initial pubdash
savedPubdash, err := service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
// attempt to overwrite settings
dto = &SavePublicDashboardDTO{
DashboardUid: dashboard.UID,
UserId: 8,
PublicDashboard: &PublicDashboardDTO{
Uid: savedPubdash.Uid,
OrgId: 9,
DashboardUid: "abc1234",
CreatedBy: 9,
CreatedAt: time.Time{},
IsEnabled: tt.IsEnabled,
AnnotationsEnabled: tt.AnnotationsEnabled,
TimeSelectionEnabled: tt.TimeSelectionEnabled,
TimeSettings: tt.TimeSettings,
Share: tt.ShareType,
AccessToken: "NOTAREALUUID",
},
}
updatedPubdash, err := service.Update(context.Background(), SignedInUser, dto)
require.NoError(t, err)
assertOldValueIfNull(t, updatedPubdash.IsEnabled, savedPubdash.IsEnabled, dto.PublicDashboard.IsEnabled)
assertOldValueIfNull(t, updatedPubdash.AnnotationsEnabled, savedPubdash.AnnotationsEnabled, dto.PublicDashboard.AnnotationsEnabled)
assertOldValueIfNull(t, updatedPubdash.TimeSelectionEnabled, savedPubdash.TimeSelectionEnabled, dto.PublicDashboard.TimeSelectionEnabled)
if dto.PublicDashboard.TimeSettings == nil {
assert.Equal(t, updatedPubdash.TimeSettings, savedPubdash.TimeSettings)
} else {
assert.Equal(t, updatedPubdash.TimeSettings, dto.PublicDashboard.TimeSettings)
}
if dto.PublicDashboard.Share == "" {
assert.Equal(t, updatedPubdash.Share, savedPubdash.Share)
} else {
assert.Equal(t, updatedPubdash.Share, dto.PublicDashboard.Share)
}
})
}
}
func assertOldValueIfNull(t *testing.T, expectedValue bool, oldValue bool, nullableValue *bool) {
if nullableValue == nil {
assert.Equal(t, expectedValue, oldValue)
} else {
assert.Equal(t, expectedValue, *nullableValue)
}
}
func TestDeletePublicDashboard(t *testing.T) {

@ -10,14 +10,14 @@ import (
func TestValidatePublicDashboard(t *testing.T) {
t.Run("Returns no error when valid shareType value is received", func(t *testing.T) {
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: &PublicDashboard{Share: EmailShareType}}
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", UserId: 1, PublicDashboard: &PublicDashboardDTO{Share: EmailShareType}}
err := ValidatePublicDashboard(dto)
require.NoError(t, err)
})
t.Run("Returns error when invalid shareType value", func(t *testing.T) {
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: &PublicDashboard{Share: "invalid"}}
dto := &SavePublicDashboardDTO{DashboardUid: "abc123", UserId: 1, PublicDashboard: &PublicDashboardDTO{Share: "invalid"}}
err := ValidatePublicDashboard(dto)
require.Error(t, err)

@ -85,23 +85,30 @@ export const publicDashboardApi = createApi({
},
invalidatesTags: (result, error, { dashboard }) => [{ type: 'PublicDashboard', id: dashboard.uid }],
}),
updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
updatePublicDashboard: builder.mutation<
PublicDashboard,
{ dashboard: Partial<DashboardModel>; payload: Partial<PublicDashboard> }
>({
query: (params) => ({
url: `/dashboards/uid/${params.dashboard.uid}/public-dashboards/${params.payload.uid}`,
method: 'PUT',
method: 'PATCH',
data: params.payload,
}),
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled;
dispatch(notifyApp(createSuccessNotification('Public dashboard updated!')));
// Update runtime meta flag
dashboard.updateMeta({
publicDashboardUid: data.uid,
publicDashboardEnabled: data.isEnabled,
});
if (dashboard.updateMeta) {
dashboard.updateMeta({
publicDashboardUid: data.uid,
publicDashboardEnabled: data.isEnabled,
});
}
},
invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }],
invalidatesTags: (result, error, { payload }) => [
{ type: 'PublicDashboard', id: payload.dashboardUid },
'AuditTablePublicDashboard',
],
}),
addRecipient: builder.mutation<void, { recipient: string; dashboardUid: string; uid: string }>({
query: () => ({

@ -306,7 +306,7 @@ describe('SharePublic - Report interactions', () => {
jest.clearAllMocks();
server.use(getExistentPublicDashboardResponse());
server.use(
rest.put('/api/dashboards/uid/:dashboardUid/public-dashboards/:uid', (req, res, ctx) =>
rest.patch('/api/dashboards/uid/:dashboardUid/public-dashboards/:uid', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({

@ -1,15 +1,9 @@
import React from 'react';
import { Page } from 'app/core/components/Page/Page';
import { PublicDashboardListTable } from './components/PublicDashboardListTable/PublicDashboardListTable';
export const ListPublicDashboardPage = ({}) => {
return (
<Page navId="dashboards/public">
<PublicDashboardListTable />
</Page>
);
return <PublicDashboardListTable />;
};
export default ListPublicDashboardPage;

@ -23,7 +23,7 @@ export const DeletePublicDashboardButton = ({
dashboard?: DashboardModel;
publicDashboard: PublicDashboardDeletion;
loader?: JSX.Element;
children: React.ReactNode;
children?: React.ReactNode;
onDismiss?: () => void;
} & ButtonProps) => {
const [deletePublicDashboard, { isLoading }] = useDeletePublicDashboardMutation();

@ -2,14 +2,14 @@ import { render, screen, waitForElementToBeRemoved, within } from '@testing-libr
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import { Provider } from 'react-redux';
import 'whatwg-fetch';
import { BrowserRouter } from 'react-router-dom';
import { TestProvider } from 'test/helpers/TestProvider';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv';
import { configureStore } from 'app/store/configureStore';
import { ListPublicDashboardResponse } from '../../types';
@ -77,23 +77,23 @@ afterEach(() => {
const selectors = e2eSelectors.pages.PublicDashboards;
const renderPublicDashboardTable = async (waitForListRendering?: boolean) => {
const store = configureStore();
const context = getGrafanaContextMock();
render(
<Provider store={store}>
<TestProvider grafanaContext={context}>
<BrowserRouter>
<PublicDashboardListTable />
</BrowserRouter>
</Provider>
</TestProvider>
);
waitForListRendering && (await waitForElementToBeRemoved(screen.getByTestId('Spinner'), { timeout: 3000 }));
waitForListRendering && (await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')[1], { timeout: 3000 }));
};
describe('Show table', () => {
it('renders loader spinner while loading', async () => {
await renderPublicDashboardTable();
const spinner = screen.getByTestId('Spinner');
const spinner = screen.getAllByTestId('Spinner')[1];
expect(spinner).toBeInTheDocument();
await waitForElementToBeRemoved(spinner);
@ -101,8 +101,7 @@ describe('Show table', () => {
it('renders public dashboard list items', async () => {
await renderPublicDashboardTable(true);
const tableBody = screen.getAllByRole('rowgroup')[1];
expect(within(tableBody).getAllByRole('row')).toHaveLength(publicDashboardListResponse.length);
expect(screen.getAllByRole('listitem')).toHaveLength(publicDashboardListResponse.length);
});
it('renders empty list', async () => {
server.use(
@ -113,8 +112,7 @@ describe('Show table', () => {
await renderPublicDashboardTable(true);
const tableBody = screen.getAllByRole('rowgroup')[1];
expect(within(tableBody).queryAllByRole('row')).toHaveLength(0);
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
});
it('renders public dashboards in a good way without trashcan', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
@ -123,15 +121,6 @@ describe('Show table', () => {
publicDashboardListResponse.forEach((pd, idx) => {
renderPublicDashboardItemCorrectly(pd, idx, false);
});
const tableBody = screen.getAllByRole('rowgroup')[1];
const tableRows = within(tableBody).getAllByRole('row');
publicDashboardListResponse.forEach((pd, idx) => {
const tableRow = tableRows[idx];
const rowDataCells = within(tableRow).getAllByRole('cell');
expect(within(rowDataCells[2]).queryByTestId(selectors.ListItem.trashcanButton)).toBeNull();
});
});
it('renders public dashboards in a good way with trashcan', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
@ -140,23 +129,6 @@ describe('Show table', () => {
publicDashboardListResponse.forEach((pd, idx) => {
renderPublicDashboardItemCorrectly(pd, idx, true);
});
const tableBody = screen.getAllByRole('rowgroup')[1];
const tableRows = within(tableBody).getAllByRole('row');
publicDashboardListResponse.forEach((pd, idx) => {
const tableRow = tableRows[idx];
const rowDataCells = within(tableRow).getAllByRole('cell');
expect(within(rowDataCells[2]).getByTestId(selectors.ListItem.trashcanButton));
});
});
it('renders public dashboards items correctly', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
await renderPublicDashboardTable(true);
publicDashboardListResponse.forEach((pd, idx) => {
renderPublicDashboardItemCorrectly(pd, idx, true);
});
});
});
@ -165,17 +137,13 @@ describe('Delete public dashboard', () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
await renderPublicDashboardTable(true);
const tableBody = screen.getAllByRole('rowgroup')[1];
expect(within(tableBody).queryAllByTestId(selectors.ListItem.trashcanButton)).toHaveLength(0);
expect(screen.queryAllByTestId(selectors.ListItem.trashcanButton)).toHaveLength(0);
});
it('when user has public dashboard write permissions, then dashboards are listed with delete button', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
await renderPublicDashboardTable(true);
const tableBody = screen.getAllByRole('rowgroup')[1];
expect(within(tableBody).getAllByTestId(selectors.ListItem.trashcanButton)).toHaveLength(
publicDashboardListResponse.length
);
expect(screen.getAllByTestId(selectors.ListItem.trashcanButton)).toHaveLength(publicDashboardListResponse.length);
});
});
@ -199,22 +167,14 @@ describe('Orphaned public dashboard', () => {
const renderPublicDashboardItemCorrectly = (pd: ListPublicDashboardResponse, idx: number, hasWriteAccess: boolean) => {
const isOrphaned = !pd.dashboardUid;
const tableBody = screen.getAllByRole('rowgroup')[1];
const tableRows = within(tableBody).getAllByRole('row');
const cardItems = screen.getAllByRole('listitem');
const tableRow = tableRows[idx];
const rowDataCells = within(tableRow).getAllByRole('cell');
expect(rowDataCells).toHaveLength(3);
const linkButton = within(cardItems[idx]).getByTestId(selectors.ListItem.linkButton);
const configButton = within(cardItems[idx]).getByTestId(selectors.ListItem.configButton);
const trashcanButton = within(cardItems[idx]).queryByTestId(selectors.ListItem.trashcanButton);
const statusTag = within(rowDataCells[1]).getByText(pd.isEnabled ? 'enabled' : 'paused');
const linkButton = within(rowDataCells[2]).getByTestId(selectors.ListItem.linkButton);
const configButton = within(rowDataCells[2]).getByTestId(selectors.ListItem.configButton);
const trashcanButton = within(rowDataCells[2]).queryByTestId(selectors.ListItem.trashcanButton);
expect(within(rowDataCells[0]).getByText(isOrphaned ? 'Orphaned public dashboard' : pd.title)).toBeInTheDocument();
expect(statusTag).toBeInTheDocument();
isOrphaned ? expect(statusTag).toHaveStyle('background-color: rgb(110, 110, 110)') : expect(statusTag).toBeEnabled();
isOrphaned || !pd.isEnabled
expect(within(cardItems[idx]).getByText(isOrphaned ? 'Orphaned public dashboard' : pd.title)).toBeInTheDocument();
isOrphaned
? expect(linkButton).toHaveStyle('pointer-events: none')
: expect(linkButton).not.toHaveStyle('pointer-events: none');
isOrphaned

@ -1,13 +1,17 @@
import { css } from '@emotion/css';
import React from 'react';
import { useWindowSize } from 'react-use';
import React, { useMemo } from 'react';
import { useMedia } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { Link, ButtonGroup, LinkButton, Icon, Tag, useStyles2, Tooltip, useTheme2, Spinner } from '@grafana/ui/src';
import { reportInteraction } from '@grafana/runtime';
import { LinkButton, useStyles2, Spinner, Card, useTheme2, Tooltip, Icon, Switch } from '@grafana/ui/src';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/services/context_srv';
import { useListPublicDashboardsQuery } from 'app/features/dashboard/api/publicDashboardApi';
import {
useListPublicDashboardsQuery,
useUpdatePublicDashboardMutation,
} from 'app/features/dashboard/api/publicDashboardApi';
import {
generatePublicDashboardConfigUrl,
generatePublicDashboardUrl,
@ -19,138 +23,160 @@ import { ListPublicDashboardResponse } from '../../types';
import { DeletePublicDashboardButton } from './DeletePublicDashboardButton';
export const PublicDashboardListTable = () => {
const { width } = useWindowSize();
const isMobile = width <= 480;
const PublicDashboardCard = ({ pd }: { pd: ListPublicDashboardResponse }) => {
const styles = useStyles2(getStyles);
const theme = useTheme2();
const styles = useStyles2(() => getStyles(theme, isMobile));
const isMobile = useMedia(`(max-width: ${theme.breakpoints.values.sm}px)`);
const { data: publicDashboards, isLoading, isFetching } = useListPublicDashboardsQuery();
const [update, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
const selectors = e2eSelectors.pages.PublicDashboards;
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const responsiveSize = isMobile ? 'sm' : 'md';
const isOrphaned = !pd.dashboardUid;
const onTogglePause = (pd: ListPublicDashboardResponse, isPaused: boolean) => {
const req = {
dashboard: { uid: pd.dashboardUid },
payload: {
uid: pd.uid,
isEnabled: !isPaused,
},
};
update(req);
};
const CardActions = useMemo(() => (isMobile ? Card.Actions : Card.SecondaryActions), [isMobile]);
return (
<Page.Contents isLoading={isLoading}>
<table className="filter-table">
<thead>
<tr>
<th className={styles.nameTh}>Name</th>
<th>Status</th>
<th className={styles.fetchingSpinner}>{isFetching && <Spinner />}</th>
</tr>
</thead>
<tbody>
{publicDashboards?.map((pd: ListPublicDashboardResponse) => {
const isOrphaned = !pd.dashboardUid;
return (
<tr key={pd.uid}>
<td className={styles.titleTd}>
<Tooltip
content={!isOrphaned ? pd.title : 'The linked dashboard has already been deleted'}
placement="top"
>
{!isOrphaned ? (
<Link className={styles.link} href={`/d/${pd.dashboardUid}`}>
{pd.title}
</Link>
) : (
<div className={styles.orphanedTitle}>
<p>Orphaned public dashboard</p>
<Icon name="info-circle" className={styles.orphanedInfoIcon} />
</div>
)}
</Tooltip>
</td>
<td>
<Tag
name={pd.isEnabled ? 'enabled' : 'paused'}
colorIndex={isOrphaned ? 9 : pd.isEnabled ? 20 : 15}
/>
</td>
<td>
<ButtonGroup className={styles.buttonGroup}>
<LinkButton
href={generatePublicDashboardUrl(pd.accessToken)}
fill="text"
size={responsiveSize}
title={pd.isEnabled ? 'View public dashboard' : 'Public dashboard is disabled'}
target="_blank"
disabled={!pd.isEnabled || isOrphaned}
data-testid={selectors.ListItem.linkButton}
>
<Icon size={responsiveSize} name="external-link-alt" />
</LinkButton>
<LinkButton
fill="text"
size={responsiveSize}
href={generatePublicDashboardConfigUrl(pd.dashboardUid)}
title="Configure public dashboard"
disabled={isOrphaned}
data-testid={selectors.ListItem.configButton}
>
<Icon size={responsiveSize} name="cog" />
</LinkButton>
{hasWritePermissions && (
<DeletePublicDashboardButton
variant="primary"
fill="text"
data-testid={selectors.ListItem.trashcanButton}
publicDashboard={pd}
loader={<Spinner />}
>
<Icon size={responsiveSize} name="trash-alt" />
</DeletePublicDashboardButton>
)}
</ButtonGroup>
</td>
</tr>
);
})}
</tbody>
</table>
</Page.Contents>
<Card className={styles.card} href={!isOrphaned ? `/d/${pd.dashboardUid}` : undefined}>
<Card.Heading className={styles.heading}>
{!isOrphaned ? (
<span>{pd.title}</span>
) : (
<Tooltip content="The linked dashboard has already been deleted" placement="top">
<div className={styles.orphanedTitle}>
<span>Orphaned public dashboard</span>
<Icon name="info-circle" />
</div>
</Tooltip>
)}
</Card.Heading>
<CardActions className={styles.actions}>
<div className={styles.pauseSwitch}>
<Switch
value={!pd.isEnabled}
label="Pause sharing"
disabled={isUpdateLoading}
onChange={(e) => {
reportInteraction('grafana_dashboards_public_enable_clicked', {
action: e.currentTarget.checked ? 'disable' : 'enable',
});
onTogglePause(pd, e.currentTarget.checked);
}}
data-testid={selectors.ListItem.pauseSwitch}
/>
<span>Pause sharing</span>
</div>
<LinkButton
disabled={isOrphaned}
fill="text"
icon="external-link-alt"
variant="secondary"
target="_blank"
color={theme.colors.warning.text}
href={generatePublicDashboardUrl(pd.accessToken)}
key="public-dashboard-url"
tooltip="View public dashboard"
data-testid={selectors.ListItem.linkButton}
/>
<LinkButton
disabled={isOrphaned}
fill="text"
icon="cog"
variant="secondary"
color={theme.colors.warning.text}
href={generatePublicDashboardConfigUrl(pd.dashboardUid)}
key="public-dashboard-config-url"
tooltip="Configure public dashboard"
data-testid={selectors.ListItem.configButton}
/>
{hasWritePermissions && (
<DeletePublicDashboardButton
fill="text"
icon="power"
variant="secondary"
publicDashboard={pd}
tooltip="Revoke public dashboard url"
loader={<Spinner />}
data-testid={selectors.ListItem.trashcanButton}
/>
)}
</CardActions>
</Card>
);
};
function getStyles(theme: GrafanaTheme2, isMobile: boolean) {
return {
fetchingSpinner: css`
display: flex;
justify-content: end;
`,
link: css`
color: ${theme.colors.primary.text};
text-decoration: underline;
margin-right: ${theme.spacing()};
`,
nameTh: css`
width: 20%;
`,
titleTd: css`
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`,
buttonGroup: css`
justify-content: ${isMobile ? 'space-between' : 'end'};
`,
orphanedTitle: css`
export const PublicDashboardListTable = () => {
const styles = useStyles2(getStyles);
const { data: publicDashboards, isLoading, isFetching } = useListPublicDashboardsQuery();
return (
<Page navId="dashboards/public" actions={isFetching && <Spinner />}>
<Page.Contents isLoading={isLoading}>
<ul className={styles.list}>
{publicDashboards?.map((pd: ListPublicDashboardResponse) => (
<li key={pd.uid}>
<PublicDashboardCard pd={pd} />
</li>
))}
</ul>
</Page.Contents>
</Page>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
list: css`
list-style-type: none;
`,
card: css`
${theme.breakpoints.up('sm')} {
display: flex;
align-items: center;
}
`,
heading: css`
display: flex;
align-items: center;
gap: ${theme.spacing(1)};
flex: 1;
`,
orphanedTitle: css`
display: flex;
align-items: center;
gap: ${theme.spacing(1)};
`,
actions: css`
display: flex;
align-items: center;
position: relative;
gap: ${theme.spacing(0.5)};
${theme.breakpoints.up('sm')} {
gap: ${theme.spacing(1)};
}
`,
pauseSwitch: css`
display: flex;
gap: ${theme.spacing(1)};
align-items: center;
font-size: ${theme.typography.bodySmall.fontSize};
margin-bottom: 0;
flex: 1;
p {
margin: ${theme.spacing(0)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`,
orphanedInfoIcon: css`
color: ${theme.colors.text.link};
`,
};
}
${theme.breakpoints.up('sm')} {
padding-right: ${theme.spacing(2)};
}
`,
});

Loading…
Cancel
Save