Dashboards: Add feature restore dashboards backend (#83131)
Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
co-authored by
Sofia Papagiannaki
parent
edae5fc791
commit
42d75ac737
@@ -7,8 +7,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
@@ -16,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
@@ -513,6 +512,67 @@ func (d *dashboardStore) GetDashboardsByPluginID(ctx context.Context, query *das
|
||||
}
|
||||
return dashboards, nil
|
||||
}
|
||||
func (d *dashboardStore) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
|
||||
if orgID == 0 || uid == "" {
|
||||
return nil, dashboards.ErrDashboardIdentifierNotSet
|
||||
}
|
||||
|
||||
var queryResult *dashboards.Dashboard
|
||||
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
dashboard := dashboards.Dashboard{OrgID: orgID, UID: uid}
|
||||
has, err := sess.Where("deleted IS NOT NULL").Get(&dashboard)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return dashboards.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
queryResult = &dashboard
|
||||
return nil
|
||||
})
|
||||
|
||||
return queryResult, err
|
||||
}
|
||||
|
||||
func (d *dashboardStore) RestoreDashboard(ctx context.Context, orgID int64, dashboardUID string, folder *folder.Folder) error {
|
||||
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
// nolint:staticcheck
|
||||
_, err := sess.Exec("UPDATE dashboard SET deleted=NULL, folder_id = ?, folder_uid=? WHERE org_id=? AND uid=?", folder.ID, folder.UID, orgID, dashboardUID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (d *dashboardStore) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
|
||||
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
_, err := sess.Exec("UPDATE dashboard SET deleted=? WHERE org_id=? AND uid=?", time.Now(), orgID, dashboardUID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (d *dashboardStore) SoftDeleteDashboardsInFolders(ctx context.Context, orgID int64, folderUids []string) error {
|
||||
if len(folderUids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
s := strings.Builder{}
|
||||
s.WriteString("UPDATE dashboard SET deleted=? WHERE ")
|
||||
s.WriteString(fmt.Sprintf("folder_uid IN (%s)", strings.Repeat("?,", len(folderUids)-1)+"?"))
|
||||
s.WriteString(" AND org_id = ? AND is_folder = ?")
|
||||
|
||||
sql := s.String()
|
||||
args := make([]any, 0, 3)
|
||||
args = append(args, sql, time.Now())
|
||||
for _, folderUID := range folderUids {
|
||||
args = append(args, folderUID)
|
||||
}
|
||||
args = append(args, orgID, d.store.GetDialect().BooleanStr(false))
|
||||
|
||||
_, err := sess.Exec(args...)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (d *dashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error {
|
||||
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
@@ -545,10 +605,13 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand,
|
||||
}
|
||||
|
||||
if dashboard.IsFolder {
|
||||
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
|
||||
// if this is a soft delete, we need to skip children deletion.
|
||||
if !d.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) && !cmd.SkipSoftDeletedDashboards {
|
||||
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ? AND deleted IS NULL")
|
||||
|
||||
if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil {
|
||||
return err
|
||||
if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// remove all access control permission with folder scope
|
||||
@@ -686,7 +749,7 @@ func (d *dashboardStore) GetDashboard(ctx context.Context, query *dashboards.Get
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||
}
|
||||
|
||||
has, err := sess.MustCols(mustCols...).Nullable("folder_uid").Get(&dashboard)
|
||||
has, err := sess.Where("deleted IS NULL").MustCols(mustCols...).Nullable("folder_uid").Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
@@ -726,17 +789,20 @@ func (d *dashboardStore) GetDashboards(ctx context.Context, query *dashboards.Ge
|
||||
if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 {
|
||||
return star.ErrCommandValidationFailed
|
||||
}
|
||||
var session *xorm.Session
|
||||
|
||||
// remove soft deleted dashboards from the response
|
||||
sess.Where("deleted IS NULL")
|
||||
|
||||
if len(query.DashboardIDs) > 0 {
|
||||
session = sess.In("id", query.DashboardIDs)
|
||||
sess.In("id", query.DashboardIDs)
|
||||
} else {
|
||||
session = sess.In("uid", query.DashboardUIDs)
|
||||
sess.In("uid", query.DashboardUIDs)
|
||||
}
|
||||
if query.OrgID > 0 {
|
||||
session = sess.Where("org_id = ?", query.OrgID)
|
||||
sess.Where("org_id = ?", query.OrgID)
|
||||
}
|
||||
|
||||
err := session.Find(&dashboards)
|
||||
err := sess.Find(&dashboards)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -802,6 +868,8 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
|
||||
})
|
||||
}
|
||||
|
||||
filters = append(filters, searchstore.DeletedFilter{Deleted: query.IsDeleted})
|
||||
|
||||
var res []dashboards.DashboardSearchProjection
|
||||
sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters, Features: d.features}
|
||||
|
||||
@@ -871,7 +939,7 @@ func (d *dashboardStore) CountDashboardsInFolders(
|
||||
args = append(args, folderUID)
|
||||
}
|
||||
}
|
||||
s.WriteString(" AND org_id = ? AND is_folder = ?")
|
||||
s.WriteString(" AND org_id = ? AND is_folder = ? AND deleted IS NULL")
|
||||
args = append(args, req.OrgID, d.store.GetDialect().BooleanStr(false))
|
||||
sql := s.String()
|
||||
_, err := sess.SQL(sql, args...).Get(&count)
|
||||
@@ -919,6 +987,18 @@ func (d *dashboardStore) GetAllDashboards(ctx context.Context) ([]*dashboards.Da
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
func (d *dashboardStore) GetSoftDeletedExpiredDashboards(ctx context.Context, duration time.Duration) ([]*dashboards.Dashboard, error) {
|
||||
var dashboards = make([]*dashboards.Dashboard, 0)
|
||||
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
err := sess.Where("deleted IS NOT NULL AND deleted < ?", time.Now().Add(-duration)).Find(&dashboards)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
||||
limits := "a.Map{}
|
||||
|
||||
|
||||
@@ -525,12 +525,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
|
||||
_ = insertTestDashboard(t, dashboardStore, "delete me 1", 1, folder.ID, folder.UID, false, "delete this 1")
|
||||
_ = insertTestDashboard(t, dashboardStore, "delete me 2", 1, folder.ID, folder.UID, false, "delete this 2")
|
||||
|
||||
err := dashboardStore.DeleteDashboardsInFolders(
|
||||
context.Background(),
|
||||
&dashboards.DeleteDashboardsInFolderRequest{
|
||||
FolderUIDs: []string{folder.UID},
|
||||
OrgID: 1,
|
||||
})
|
||||
err := dashboardStore.SoftDeleteDashboardsInFolders(context.Background(), folder.OrgID, []string{folder.UID})
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{folder.UID}, OrgID: 1})
|
||||
@@ -539,6 +534,127 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationGetSoftDeletedDashboard(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
var sqlStore *sqlstore.SQLStore
|
||||
var cfg *setting.Cfg
|
||||
var savedFolder, savedDash *dashboards.Dashboard
|
||||
var dashboardStore dashboards.Store
|
||||
|
||||
setup := func() {
|
||||
sqlStore, cfg = db.InitTestDBWithCfg(t)
|
||||
quotaService := quotatest.New(false, nil)
|
||||
var err error
|
||||
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService)
|
||||
require.NoError(t, err)
|
||||
savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
|
||||
savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp")
|
||||
insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, savedFolder.UID, false, "prod")
|
||||
}
|
||||
|
||||
t.Run("Should soft delete a dashboard", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
// Confirm there are 2 dashboards in the folder
|
||||
amount, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), amount)
|
||||
|
||||
// Soft delete the dashboard
|
||||
err = dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// There is only 1 dashboard in the folder after soft delete
|
||||
amount, err = dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), amount)
|
||||
|
||||
var dash *dashboards.Dashboard
|
||||
// Get the soft deleted dashboard should be empty
|
||||
dash, _ = dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{UID: savedDash.UID, OrgID: savedDash.OrgID})
|
||||
assert.Error(t, dashboards.ErrDashboardNotFound)
|
||||
assert.Nil(t, dash)
|
||||
|
||||
// Get the soft deleted dashboard
|
||||
dash, err = dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, savedDash.ID, dash.ID)
|
||||
assert.Equal(t, savedDash.UID, dash.UID)
|
||||
assert.Equal(t, savedDash.Title, dash.Title)
|
||||
})
|
||||
|
||||
t.Run("Should not fail when trying to soft delete a soft deleted dashboard", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
// Soft delete the dashboard
|
||||
err := dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Soft delete the dashboard
|
||||
err = dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the soft deleted dashboard
|
||||
dash, err := dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, savedDash.ID, dash.ID)
|
||||
assert.Equal(t, savedDash.UID, dash.UID)
|
||||
assert.Equal(t, savedDash.Title, dash.Title)
|
||||
})
|
||||
|
||||
t.Run("Should restore a dashboard", func(t *testing.T) {
|
||||
setup()
|
||||
|
||||
// Confirm there are 2 dashboards in the folder
|
||||
amount, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), amount)
|
||||
|
||||
// Soft delete the dashboard
|
||||
err = dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// There is only 1 dashboard in the folder after soft delete
|
||||
amount, err = dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), amount)
|
||||
|
||||
// Get the soft deleted dashboard
|
||||
dash, err := dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, savedDash.ID, dash.ID)
|
||||
assert.Equal(t, savedDash.UID, dash.UID)
|
||||
assert.Equal(t, savedDash.Title, dash.Title)
|
||||
|
||||
// Restore deleted dashboard
|
||||
// nolint:staticcheck
|
||||
err = dashboardStore.RestoreDashboard(context.Background(), savedDash.OrgID, savedDash.UID, &folder.Folder{ID: savedDash.FolderID, UID: savedDash.FolderUID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Restore increases the amount of dashboards in the folder
|
||||
amount, err = dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), amount)
|
||||
|
||||
// Get the soft deleted dashboard should be empty
|
||||
dash, err = dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, dash)
|
||||
|
||||
// Get the restored dashboard
|
||||
dash, err = dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{UID: savedDash.UID, OrgID: savedDash.OrgID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, savedDash.ID, dash.ID)
|
||||
assert.Equal(t, savedDash.UID, dash.UID)
|
||||
assert.Equal(t, savedDash.Title, dash.Title)
|
||||
// nolint:staticcheck
|
||||
assert.Equal(t, savedDash.FolderID, dash.FolderID)
|
||||
assert.Equal(t, savedDash.FolderUID, dash.FolderUID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
|
||||
Reference in New Issue
Block a user