Dashboard Restore: Remove experimental functionality under feature flag `dashboardRestore` for now - this will be reworked (#103204)

pull/103332/head
Stephanie Hingtgen 3 months ago committed by GitHub
parent db1f1c5df9
commit 4918d8720c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 101
      docs/sources/developers/http_api/dashboard.md
  2. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  3. 5
      packages/grafana-data/src/types/featureToggles.gen.ts
  4. 14
      pkg/api/api.go
  5. 113
      pkg/api/dashboard.go
  6. 2
      pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go
  7. 2
      pkg/registry/apis/dashboard/legacy/migrate.go
  8. 13
      pkg/registry/apis/dashboard/legacy/sql_dashboards.go
  9. 3
      pkg/registry/apis/dashboard/register.go
  10. 11
      pkg/services/cleanup/cleanup.go
  11. 28
      pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go
  12. 6
      pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go
  13. 10
      pkg/services/dashboards/dashboard.go
  14. 94
      pkg/services/dashboards/dashboard_service_mock.go
  15. 101
      pkg/services/dashboards/database/database.go
  16. 123
      pkg/services/dashboards/database/database_test.go
  17. 98
      pkg/services/dashboards/service/dashboard_service.go
  18. 7
      pkg/services/dashboards/service/dashboard_service_test.go
  19. 117
      pkg/services/dashboards/store_mock.go
  20. 8
      pkg/services/featuremgmt/registry.go
  21. 1
      pkg/services/featuremgmt/toggles-gitlog.csv
  22. 1
      pkg/services/featuremgmt/toggles_gen.csv
  23. 4
      pkg/services/featuremgmt/toggles_gen.go
  24. 17
      pkg/services/featuremgmt/toggles_gen.json
  25. 3
      pkg/services/folder/folderimpl/folder.go
  26. 56
      pkg/services/folder/folderimpl/folder_test.go
  27. 6
      pkg/services/folder/folderimpl/folder_unifiedstorage_test.go
  28. 9
      pkg/services/navtree/navtreeimpl/navtree.go
  29. 83
      pkg/services/searchV2/index_test.go
  30. 78
      public/api-merged.json
  31. 5
      public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
  32. 3
      public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
  33. 2
      public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
  34. 6
      public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx
  35. 4
      public/app/features/dashboard-scene/saving/shared.tsx
  36. 6
      public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx
  37. 4
      public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx
  38. 4
      public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx
  39. 7
      public/app/routes/routes.tsx
  40. 85
      public/openapi3.json

@ -260,107 +260,6 @@ Status Codes:
- **403** – Access denied
- **404** – Not found
## Hard delete dashboard by uid
{{% admonition type="note" %}}
This feature is currently in private preview and behind the `dashboardRestore` feature toggle.
{{% /admonition %}}
`DELETE /api/dashboards/uid/:uid/trash`
Will delete permanently the dashboard given the specified unique identifier (uid).
**Required permissions**
See note in the [introduction](#dashboard-api) for an explanation.
<!-- prettier-ignore-start -->
| Action | Scope |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| `dashboards:delete` | <ul><li>`dashboards:*`</li><li>`dashboards:uid:*`</li><li>`folders:*`</li><li>`folders:uid:*`</li></ul> |
{ .no-spacing-list }
<!-- prettier-ignore-end -->
**Example Request**:
```http
DELETE /api/dashboards/uid/cIBgcSjkk/trash HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"title": "Production Overview",
"message": "Dashboard Production Overview deleted",
"uid": "cIBgcSjkk"
}
```
Status Codes:
- **200** – Deleted
- **401** – Unauthorized
- **403** – Access denied
- **404** – Not found
## Restore deleted dashboard by uid
{{% admonition type="note" %}}
This feature is currently in private preview and behind the `dashboardRestore` feature toggle.
{{% /admonition %}}
`PATCH /api/dashboards/uid/:uid/trash`
Will restore a deleted dashboard given the specified unique identifier (uid).
**Required permissions**
See note in the [introduction](#dashboard-api) for an explanation.
<!-- prettier-ignore-start -->
| Action | Scope |
| ------------------- | ----------------------------------------------------- |
| `dashboards:create` | <ul><li>`folders:*`</li><li>`folders:uid:*`</li></ul> |
{ .no-spacing-list }
<!-- prettier-ignore-end -->
**Example Request**:
```http
PATCH /api/dashboards/uid/cIBgcSjkk/trash HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
{
"title": "Production Overview",
"message": "Dashboard Production Overview restored",
"uid": "cIBgcSjkk"
}
```
Status Codes:
- **200** – Restored
- **401** – Unauthorized
- **403** – Access denied
- **404** – Not found
-
## Gets the home dashboard
`GET /api/dashboards/home`

@ -194,7 +194,6 @@ Experimental features might be changed or removed without prior notice.
| `queryLibrary` | Enables Query Library feature in Explore |
| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore |
| `alertingListViewV2` | Enables the new alert list view design |
| `dashboardRestore` | Enables deleted dashboard restore feature |
| `alertingCentralAlertHistory` | Enables the new central alert history. |
| `dataplaneAggregator` | Enable grafana dataplane aggregator |
| `tableNextGen` | Allows access to the new react-data-grid based table component. |

@ -669,11 +669,6 @@ export interface FeatureToggles {
*/
alertingListViewV2?: boolean;
/**
* Enables deleted dashboard restore feature
* @default false
*/
dashboardRestore?: boolean;
/**
* Disables the ability to send alerts to an external Alertmanager datasource.
*/
alertingDisableSendAlertsExternal?: boolean;

@ -181,10 +181,6 @@ func (hs *HTTPServer) registerRoutes() {
)
}
if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
r.Get("/dashboard/recently-deleted", reqOrgAdmin, hs.Index)
}
r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index)
r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index)
@ -466,23 +462,13 @@ func (hs *HTTPServer) registerRoutes() {
dashUIDScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(ac.Parameter(":uid"))
dashboardRoute.Get("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsRead, dashUIDScope)), routing.Wrap(hs.GetDashboard))
if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.SoftDeleteDashboard))
} else {
dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.DeleteDashboardByUID))
}
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
dashUidRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersions))
dashUidRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDashboardVersion))
dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersion))
if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
dashUidRoute.Patch("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDeletedDashboard))
dashUidRoute.Delete("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.HardDeleteDashboardByUID))
}
dashUidRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList))
dashboardPermissionRoute.Post("/", authorize(ac.EvalPermission(dashboards.ActionDashboardsPermissionsWrite)), routing.Wrap(hs.UpdateDashboardPermissions))

@ -344,91 +344,6 @@ func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id in
return queryResult, nil
}
// swagger:route PATCH /dashboards/uid/{uid}/trash dashboards restoreDeletedDashboardByUID
//
// Restore a dashboard to a given dashboard version using UID.
//
// Responses:
// 200: postDashboardResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) response.Response {
ctx, span := tracer.Start(c.Req.Context(), "api.RestoreDeletedDashboard")
defer span.End()
c.Req = c.Req.WithContext(ctx)
uid := web.Params(c.Req)[":uid"]
cmd := dashboards.RestoreDeletedDashboardCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
dash, err := hs.DashboardService.GetSoftDeletedDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid)
if err != nil {
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
err = hs.DashboardService.RestoreDashboard(c.Req.Context(), dash, c.SignedInUser, cmd.FolderUID)
if err != nil {
var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err)
}
return response.Error(http.StatusInternalServerError, "Dashboard cannot be restored", err)
}
return response.JSON(http.StatusOK, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s restored", dash.Title),
"uid": dash.UID,
})
}
// SoftDeleteDashboard swagger:route DELETE /dashboards/uid/{uid} dashboards deleteDashboardByUID
//
// Delete dashboard by uid.
//
// Will delete the dashboard given the specified unique identifier (uid).
//
// Responses:
// 200: deleteDashboardResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.Response {
ctx, span := tracer.Start(c.Req.Context(), "api.SoftDeleteDashboard")
defer span.End()
c.Req = c.Req.WithContext(ctx)
uid := web.Params(c.Req)[":uid"]
dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, uid)
if rsp != nil {
return rsp
}
err := hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid)
if err != nil {
var dashboardErr dashboardaccess.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
if errors.Is(err, dashboards.ErrDashboardCannotDeleteProvisionedDashboard) {
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err)
}
}
return response.Error(http.StatusInternalServerError, "Failed to delete dashboard", err)
}
return response.JSON(http.StatusOK, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s moved to Recently deleted", dash.Title),
"uid": dash.UID,
})
}
// DeleteDashboardByUID swagger:route DELETE /dashboards/uid/{uid} dashboards deleteDashboardByUID
//
// Delete dashboard by uid.
@ -445,22 +360,6 @@ func (hs *HTTPServer) DeleteDashboardByUID(c *contextmodel.ReqContext) response.
return hs.deleteDashboard(c)
}
// HardDeleteDashboardByUID swagger:route DELETE /dashboards/uid/{uid}/trash dashboards hardDeleteDashboardByUID
//
// Hard delete dashboard by uid.
//
// Will delete the dashboard given the specified unique identifier (uid).
//
// Responses:
// 200: deleteDashboardResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) HardDeleteDashboardByUID(c *contextmodel.ReqContext) response.Response {
return hs.deleteDashboard(c)
}
func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Response {
ctx, span := tracer.Start(c.Req.Context(), "api.deleteDashboard")
defer span.End()
@ -468,21 +367,11 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
uid := web.Params(c.Req)[":uid"]
var dash *dashboards.Dashboard
if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
var err error
dash, err = hs.DashboardService.GetSoftDeletedDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid)
if err != nil {
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
} else {
var rsp response.Response
dash, rsp = hs.getDashboardHelper(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, web.Params(c.Req)[":uid"])
dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, uid)
if rsp != nil {
return rsp
}
}
if dash.IsFolder {
return response.Error(http.StatusBadRequest, "Use folders endpoint for deleting folders.", nil)
}

@ -64,7 +64,7 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
migrator := legacy.NewDashboardAccess(
legacysql.NewDatabaseProvider(sqlStore),
authlib.OrgNamespaceFormatter,
nil, provisioning, false, sort.ProvideService(),
nil, provisioning, sort.ProvideService(),
)
yes, err := promptYesNo(fmt.Sprintf("Count legacy resources for namespace: %s?", opts.Namespace))

@ -45,7 +45,7 @@ func ProvideLegacyMigrator(
provisioning provisioning.ProvisioningService, // only needed for dashboard settings
) LegacyMigrator {
dbp := legacysql.NewDatabaseProvider(sql)
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, false, sort.ProvideService())
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, sort.ProvideService())
}
type BlobStoreInfo struct {

@ -57,7 +57,6 @@ type dashboardSqlAccess struct {
// Use for writing (not reading)
dashStore dashboards.Store
softDelete bool
dashboardSearchClient legacysearcher.DashboardSearchClient
// Typically one... the server wrapper
@ -69,7 +68,6 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
namespacer request.NamespaceMapper,
dashStore dashboards.Store,
provisioning provisioning.ProvisioningService,
softDelete bool,
sorter sort.Service,
) DashboardAccess {
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
@ -78,7 +76,6 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
softDelete: softDelete,
dashboardSearchClient: *dashboardSearchClient,
}
}
@ -367,16 +364,6 @@ func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, u
return nil, false, err
}
if a.softDelete {
err = a.dashStore.SoftDeleteDashboard(ctx, orgId, uid)
if err == nil && dash != nil {
now := metav1.NewTime(time.Now())
dash.DeletionTimestamp = &now
return dash, true, err
}
return dash, false, err
}
err = a.dashStore.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{
OrgID: orgId,
UID: uid,

@ -85,7 +85,6 @@ func RegisterAPIService(
dual dualwrite.Service,
sorter sort.Service,
) *DashboardsAPIBuilder {
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
@ -100,7 +99,7 @@ func RegisterAPIService(
search: NewSearchHandler(tracing, dual, legacyDashboardSearcher, unified, features),
legacy: &DashboardStorage{
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, sorter),
},
reg: reg,
}

@ -111,7 +111,6 @@ func (srv *CleanUpService) clean(ctx context.Context) {
{"expire old user invites", srv.expireOldUserInvites},
{"delete stale query history", srv.deleteStaleQueryHistory},
{"expire old email verifications", srv.expireOldVerifications},
{"cleanup trash dashboards", srv.cleanUpTrashDashboards},
}
if srv.Cfg.ShortLinkExpiration > 0 {
@ -314,16 +313,6 @@ func (srv *CleanUpService) deleteStaleQueryHistory(ctx context.Context) {
}
}
func (srv *CleanUpService) cleanUpTrashDashboards(ctx context.Context) {
logger := srv.log.FromContext(ctx)
affected, err := srv.dashboardService.CleanUpDeletedDashboards(ctx)
if err != nil {
logger.Error("Problem cleaning up deleted dashboards", "error", err)
} else {
logger.Debug("Cleaned up deleted dashboards", "dashboards affected", affected)
}
}
func (srv *CleanUpService) cleanUpTrashAlertRules(ctx context.Context) {
logger := srv.log.FromContext(ctx)
affected, err := srv.alertRuleService.CleanUpDeletedAlertRules(ctx)

@ -432,32 +432,6 @@ func Test_OnlyQueriesStatusFromGMSWhenRequired(t *testing.T) {
assert.Never(t, func() bool { return gmsClientMock.GetSnapshotStatusCallCount() > 2 }, time.Second, 10*time.Millisecond)
}
func Test_DeletedDashboardsNotMigrated(t *testing.T) {
t.Parallel()
s := setUpServiceTest(t, false).(*Service)
// modify what the mock returns for just this test case
dashMock := s.dashboardService.(*dashboards.FakeDashboardService)
dashMock.On("GetAllDashboardsByOrgId", mock.Anything, int64(1)).Return(
[]*dashboards.Dashboard{
{UID: "1", OrgID: 1, Data: simplejson.New()},
{UID: "2", OrgID: 1, Data: simplejson.New(), Deleted: time.Now()},
},
nil,
)
data, err := s.getMigrationDataJSON(context.TODO(), &user.SignedInUser{OrgID: 1})
assert.NoError(t, err)
dashCount := 0
for _, it := range data.Items {
if it.Type == cloudmigration.DashboardDataType {
dashCount++
}
}
assert.Equal(t, 1, dashCount)
}
// Implementation inspired by ChatGPT, OpenAI's language model.
func Test_SortFolders(t *testing.T) {
folders := []folder.CreateFolderCommand{
@ -932,14 +906,12 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf
featureToggles := featuremgmt.WithFeatures(
featuremgmt.FlagOnPremToCloudMigrations,
featuremgmt.FlagDashboardRestore, // needed for skipping creating soft-deleted dashboards in the snapshot.
)
sqlStore := sqlstore.NewTestStore(t,
sqlstore.WithCfg(cfg),
sqlstore.WithFeatureFlags(
featuremgmt.FlagOnPremToCloudMigrations,
featuremgmt.FlagDashboardRestore, // needed for skipping creating soft-deleted dashboards in the snapshot.
),
)

@ -24,7 +24,6 @@ import (
"github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
@ -318,15 +317,10 @@ func (s *Service) getDashboardAndFolderCommands(ctx context.Context, signedInUse
dashboardCmds := make([]dashboards.Dashboard, 0)
folderUids := make([]string, 0)
softDeleteEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
// Folders need to be fetched by UID in a separate step, separate dashboards from folders
// If any result is in the trash bin, don't migrate it
for _, d := range dashs {
if softDeleteEnabled && !d.Deleted.IsZero() {
continue
}
if d.IsFolder {
folderUids = append(folderUids, d.UID)
} else {

@ -2,7 +2,6 @@ package dashboards
import (
"context"
"time"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@ -32,11 +31,7 @@ type DashboardService interface {
CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error)
GetAllDashboards(ctx context.Context) ([]*Dashboard, error)
GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error)
SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUid string) error
RestoreDashboard(ctx context.Context, dashboard *Dashboard, user identity.Requester, optionalFolderUID string) error
CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error
CleanUpDeletedDashboards(ctx context.Context) (int64, error)
GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*Dashboard, error)
CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error)
}
@ -98,9 +93,4 @@ type Store interface {
GetAllDashboards(ctx context.Context) ([]*Dashboard, error)
GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error)
GetSoftDeletedExpiredDashboards(ctx context.Context, duration time.Duration) ([]*Dashboard, error)
SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUid string) error
SoftDeleteDashboardsInFolders(ctx context.Context, orgID int64, folderUids []string) error
RestoreDashboard(ctx context.Context, orgID int64, dashboardUid string, folder *folder.Folder) error
GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*Dashboard, error)
}

@ -46,34 +46,6 @@ func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, d
return r0, r1
}
// CleanUpDeletedDashboards provides a mock function with given fields: ctx
func (_m *FakeDashboardService) CleanUpDeletedDashboards(ctx context.Context) (int64, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for CleanUpDeletedDashboards")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (int64, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) int64); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountDashboardsInOrg provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
ret := _m.Called(ctx, orgID)
@ -376,36 +348,6 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDas
return r0, r1
}
// GetSoftDeletedDashboard provides a mock function with given fields: ctx, orgID, uid
func (_m *FakeDashboardService) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*Dashboard, error) {
ret := _m.Called(ctx, orgID, uid)
if len(ret) == 0 {
panic("no return value specified for GetSoftDeletedDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*Dashboard, error)); ok {
return rf(ctx, orgID, uid)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *Dashboard); ok {
r0 = rf(ctx, orgID, uid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*Dashboard)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgID, uid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ImportDashboard provides a mock function with given fields: ctx, dto
func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) {
ret := _m.Called(ctx, dto)
@ -436,24 +378,6 @@ func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDa
return r0, r1
}
// RestoreDashboard provides a mock function with given fields: ctx, dashboard, user, optionalFolderUID
func (_m *FakeDashboardService) RestoreDashboard(ctx context.Context, dashboard *Dashboard, user identity.Requester, optionalFolderUID string) error {
ret := _m.Called(ctx, dashboard, user, optionalFolderUID)
if len(ret) == 0 {
panic("no return value specified for RestoreDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *Dashboard, identity.Requester, string) error); ok {
r0 = rf(ctx, dashboard, user, optionalFolderUID)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveDashboard provides a mock function with given fields: ctx, dto, allowUiUpdate
func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) {
ret := _m.Called(ctx, dto, allowUiUpdate)
@ -514,24 +438,6 @@ func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *Fin
return r0, r1
}
// SoftDeleteDashboard provides a mock function with given fields: ctx, orgID, dashboardUid
func (_m *FakeDashboardService) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUid string) error {
ret := _m.Called(ctx, orgID, dashboardUid)
if len(ret) == 0 {
panic("no return value specified for SoftDeleteDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok {
r0 = rf(ctx, orgID, dashboardUid)
} else {
r0 = ret.Error(0)
}
return r0
}
// CleanUpDashboard provides a mock function with given fields: ctx, dashboardUID, orgId
func (_m *FakeDashboardService) CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error {
ret := _m.Called(ctx, dashboardUID, orgId)

@ -19,7 +19,6 @@ 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/migrations"
@ -564,83 +563,6 @@ 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) {
ctx, span := tracer.Start(ctx, "dashboards.database.GetSoftDeletedDashboard")
defer span.End()
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 {
ctx, span := tracer.Start(ctx, "dashboards.database.RestoreDashboard")
defer span.End()
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
if folder == nil || folder.UID == "" {
_, err := sess.Exec("UPDATE dashboard SET deleted=NULL, folder_id=0, folder_uid=NULL WHERE org_id=? AND uid=?", orgID, dashboardUID)
return err
}
// 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 {
ctx, span := tracer.Start(ctx, "dashboards.database.SoftDeleteDashboard")
defer span.End()
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 {
ctx, span := tracer.Start(ctx, "dashboards.database.SoftDeleteDashboardsInFolders")
defer span.End()
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().BooleanValue(false))
_, err := sess.Exec(args...)
return err
})
}
func (d *dashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error {
ctx, span := tracer.Start(ctx, "dashboards.database.DeleteDashboard")
@ -681,7 +603,6 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand,
}
if dashboard.IsFolder {
if !d.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
sqlStatements = append(sqlStatements, statement{
SQL: "DELETE FROM dashboard WHERE org_id = ? AND folder_uid = ? AND is_folder = ? AND deleted IS NULL",
args: []any{dashboard.OrgID, dashboard.UID, d.store.GetDialect().BooleanValue(false)},
@ -690,13 +611,6 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand,
if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil {
return err
}
} else {
// soft delete all dashboards in the folder
sqlStatements = append(sqlStatements, statement{
SQL: "UPDATE dashboard SET deleted = ? WHERE org_id = ? AND folder_uid = ? AND is_folder = ? ",
args: []any{time.Now(), dashboard.OrgID, dashboard.UID, d.store.GetDialect().BooleanValue(false)},
})
}
// remove all access control permission with folder scope
err := d.deleteResourcePermissions(sess, dashboard.OrgID, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.UID))
@ -1164,18 +1078,3 @@ func (d *dashboardStore) GetAllDashboardsByOrgId(ctx context.Context, orgID int6
}
return dashs, nil
}
func (d *dashboardStore) GetSoftDeletedExpiredDashboards(ctx context.Context, duration time.Duration) ([]*dashboards.Dashboard, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.GetSoftDeletedExpiredDashboards")
defer span.End()
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
}

@ -25,7 +25,6 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
@ -702,7 +701,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.SoftDeleteDashboardsInFolders(context.Background(), folder.OrgID, []string{folder.UID})
err := dashboardStore.DeleteDashboardsInFolders(context.Background(), &dashboards.DeleteDashboardsInFolderRequest{OrgID: folder.OrgID, FolderUIDs: []string{folder.UID}})
require.NoError(t, err)
count, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{folder.UID}, OrgID: 1})
@ -711,126 +710,6 @@ 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)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
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")

@ -1036,79 +1036,6 @@ func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboar
return dr.dashboardStore.SaveDashboard(ctx, *cmd)
}
func (dr *DashboardServiceImpl) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: orgID, UID: uid})
}
return dr.dashboardStore.GetSoftDeletedDashboard(ctx, orgID, uid)
}
func (dr *DashboardServiceImpl) RestoreDashboard(ctx context.Context, dashboard *dashboards.Dashboard, user identity.Requester, optionalFolderUID string) error {
ctx, span := tracer.Start(ctx, "dashboards.service.RestoreDashboard")
defer span.End()
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
}
// if the optionalFolder is provided we need to check if the folder exists and user has access to it
if optionalFolderUID != "" {
restoringFolder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{
UID: &optionalFolderUID,
OrgID: dashboard.OrgID,
SignedInUser: user,
})
if err != nil {
if errors.Is(err, dashboards.ErrFolderNotFound) {
return dashboards.ErrFolderRestoreNotFound
}
return folder.ErrInternal.Errorf("failed to fetch parent folder from store: %w", err)
}
return dr.dashboardStore.RestoreDashboard(ctx, dashboard.OrgID, dashboard.UID, restoringFolder)
}
// if the optionalFolder is not provided we need to restore the dashboard to the original folder
// we check for permissions and the folder existence before restoring
restoringFolder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{
UID: &dashboard.FolderUID,
OrgID: dashboard.OrgID,
SignedInUser: user,
})
if err != nil {
if errors.Is(err, dashboards.ErrFolderNotFound) {
return dashboards.ErrFolderRestoreNotFound
}
return folder.ErrInternal.Errorf("failed to fetch parent folder from store: %w", err)
}
// TODO: once restore in k8s is finalized, add functionality here under the feature toggle
return dr.dashboardStore.RestoreDashboard(ctx, dashboard.OrgID, dashboard.UID, restoringFolder)
}
func (dr *DashboardServiceImpl) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
ctx, span := tracer.Start(ctx, "dashboards.service.SoftDeleteDashboard")
defer span.End()
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
}
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
// deletes in unistore are soft deletes, so we can just delete in the same way
return dr.deleteDashboardThroughK8s(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgID, UID: dashboardUID}, true)
}
provisionedData, _ := dr.GetProvisionedDashboardDataByDashboardUID(ctx, orgID, dashboardUID)
if provisionedData != nil && provisionedData.ID != 0 {
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
}
return dr.dashboardStore.SoftDeleteDashboard(ctx, orgID, dashboardUID)
}
// DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for
// operations by the user where we want to make sure user does not delete provisioned dashboard.
func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error {
@ -1745,10 +1672,6 @@ func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64
ctx, span := tracer.Start(ctx, "dashboards.service.DeleteInFolders")
defer span.End()
if dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
return dr.dashboardStore.SoftDeleteDashboardsInFolders(ctx, orgID, folderUIDs)
}
// We need a list of dashboard uids inside the folder to delete related public dashboards
dashes, err := dr.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: u,
@ -1788,27 +1711,6 @@ func (dr *DashboardServiceImpl) CleanUpDashboard(ctx context.Context, dashboardU
return dr.dashboardStore.CleanupAfterDelete(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgId, UID: dashboardUID})
}
func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (int64, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.CleanUpDeletedDashboards")
defer span.End()
var deletedDashboardsCount int64
deletedDashboards, err := dr.dashboardStore.GetSoftDeletedExpiredDashboards(ctx, daysInTrash)
if err != nil {
return 0, err
}
for _, dashboard := range deletedDashboards {
err = dr.DeleteDashboard(ctx, dashboard.ID, dashboard.UID, dashboard.OrgID)
if err != nil {
dr.log.Warn("Failed to cleanup deleted dashboard", "dashboardUid", dashboard.UID, "error", err)
break
}
deletedDashboardsCount++
}
return deletedDashboardsCount, nil
}
// -----------------------------------------------------------------------------------------
// Dashboard k8s functions
// -----------------------------------------------------------------------------------------

@ -255,13 +255,6 @@ func TestDashboardService(t *testing.T) {
err := service.DeleteInFolders(context.Background(), 1, []string{"uid"}, nil)
require.NoError(t, err)
})
t.Run("Soft Delete dashboards in folder", func(t *testing.T) {
service.features = featuremgmt.WithFeatures(featuremgmt.FlagDashboardRestore)
fakeStore.On("SoftDeleteDashboardsInFolders", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
err := service.DeleteInFolders(context.Background(), 1, []string{"uid"}, nil)
require.NoError(t, err)
})
})
}

@ -5,12 +5,9 @@ package dashboards
import (
context "context"
folder "github.com/grafana/grafana/pkg/services/folder"
mock "github.com/stretchr/testify/mock"
quota "github.com/grafana/grafana/pkg/services/quota"
time "time"
)
// FakeDashboardStore is an autogenerated mock type for the Store type
@ -586,84 +583,6 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(ctx context.Conte
return r0, r1
}
// GetSoftDeletedDashboard provides a mock function with given fields: ctx, orgID, uid
func (_m *FakeDashboardStore) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*Dashboard, error) {
ret := _m.Called(ctx, orgID, uid)
if len(ret) == 0 {
panic("no return value specified for GetSoftDeletedDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*Dashboard, error)); ok {
return rf(ctx, orgID, uid)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *Dashboard); ok {
r0 = rf(ctx, orgID, uid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*Dashboard)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgID, uid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSoftDeletedExpiredDashboards provides a mock function with given fields: ctx, duration
func (_m *FakeDashboardStore) GetSoftDeletedExpiredDashboards(ctx context.Context, duration time.Duration) ([]*Dashboard, error) {
ret := _m.Called(ctx, duration)
if len(ret) == 0 {
panic("no return value specified for GetSoftDeletedExpiredDashboards")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, time.Duration) ([]*Dashboard, error)); ok {
return rf(ctx, duration)
}
if rf, ok := ret.Get(0).(func(context.Context, time.Duration) []*Dashboard); ok {
r0 = rf(ctx, duration)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*Dashboard)
}
}
if rf, ok := ret.Get(1).(func(context.Context, time.Duration) error); ok {
r1 = rf(ctx, duration)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RestoreDashboard provides a mock function with given fields: ctx, orgID, dashboardUid, _a3
func (_m *FakeDashboardStore) RestoreDashboard(ctx context.Context, orgID int64, dashboardUid string, _a3 *folder.Folder) error {
ret := _m.Called(ctx, orgID, dashboardUid, _a3)
if len(ret) == 0 {
panic("no return value specified for RestoreDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string, *folder.Folder) error); ok {
r0 = rf(ctx, orgID, dashboardUid, _a3)
} else {
r0 = ret.Error(0)
}
return r0
}
// SaveDashboard provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) {
ret := _m.Called(ctx, cmd)
@ -724,42 +643,6 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd
return r0, r1
}
// SoftDeleteDashboard provides a mock function with given fields: ctx, orgID, dashboardUid
func (_m *FakeDashboardStore) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUid string) error {
ret := _m.Called(ctx, orgID, dashboardUid)
if len(ret) == 0 {
panic("no return value specified for SoftDeleteDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok {
r0 = rf(ctx, orgID, dashboardUid)
} else {
r0 = ret.Error(0)
}
return r0
}
// SoftDeleteDashboardsInFolders provides a mock function with given fields: ctx, orgID, folderUids
func (_m *FakeDashboardStore) SoftDeleteDashboardsInFolders(ctx context.Context, orgID int64, folderUids []string) error {
ret := _m.Called(ctx, orgID, folderUids)
if len(ret) == 0 {
panic("no return value specified for SoftDeleteDashboardsInFolders")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, []string) error); ok {
r0 = rf(ctx, orgID, folderUids)
} else {
r0 = ret.Error(0)
}
return r0
}
// UnprovisionDashboard provides a mock function with given fields: ctx, id
func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)

@ -1146,14 +1146,6 @@ var (
Owner: grafanaAlertingSquad,
FrontendOnly: true,
},
{
Name: "dashboardRestore",
Description: "Enables deleted dashboard restore feature",
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
HideFromAdminPage: true,
Expression: "false", // enabled by default
},
{
Name: "alertingDisableSendAlertsExternal",
Description: "Disables the ability to send alerts to an external Alertmanager datasource.",

@ -319,7 +319,6 @@ tlsMemcached,2024-05-09T19:12:08Z,,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean
notificationBanner,2024-05-13T09:32:34Z,2025-01-10T10:18:43Z,f3953b4955c218cc4678e842faa3d3380fd0f8f7,Alex Khomenko
dualWritePlaylistsMode2,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R
dualWritePlaylistsMode3,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R
dashboardRestore,2024-05-16T17:36:26Z,,42d75ac737d7ac001a6d53376e25512408a01db5,Ezequiel Victorero
datasourceProxyDisableRBAC,2024-05-21T13:05:16Z,,0072e4a92d896df343d9586522d6f7533773da78,Aaron Godin
alertingDisableSendAlertsExternal,2024-05-23T12:29:19Z,,8421919cb552b9e8dbd4ebba20bcc67bbc5e6b4f,Steve Simpson
datasourceQueryTypes,2024-05-23T16:46:28Z,,42b0f802de31f3d42ece68544c193a69ea381bc0,Ryan McKinley

1 #name created deleted hash author
319 notificationBanner 2024-05-13T09:32:34Z 2025-01-10T10:18:43Z f3953b4955c218cc4678e842faa3d3380fd0f8f7 Alex Khomenko
320 dualWritePlaylistsMode2 2024-05-14T12:11:56Z 2024-05-31T18:18:09Z 6836bfe1ea1bf62f4eb66dc1328a456183874f81 Arati R
321 dualWritePlaylistsMode3 2024-05-14T12:11:56Z 2024-05-31T18:18:09Z 6836bfe1ea1bf62f4eb66dc1328a456183874f81 Arati R
dashboardRestore 2024-05-16T17:36:26Z 42d75ac737d7ac001a6d53376e25512408a01db5 Ezequiel Victorero
322 datasourceProxyDisableRBAC 2024-05-21T13:05:16Z 0072e4a92d896df343d9586522d6f7533773da78 Aaron Godin
323 alertingDisableSendAlertsExternal 2024-05-23T12:29:19Z 8421919cb552b9e8dbd4ebba20bcc67bbc5e6b4f Steve Simpson
324 datasourceQueryTypes 2024-05-23T16:46:28Z 42b0f802de31f3d42ece68544c193a69ea381bc0 Ryan McKinley

@ -149,7 +149,6 @@ queryLibrary,experimental,@grafana/grafana-frontend-platform,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true
alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true
dashboardRestore,experimental,@grafana/search-and-storage,false,false,false
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
149 logsExploreTableDefaultVisualization experimental @grafana/observability-logs false false true
150 newDashboardSharingComponent GA @grafana/sharing-squad false false true
151 alertingListViewV2 experimental @grafana/alerting-squad false false true
dashboardRestore experimental @grafana/search-and-storage false false false
152 alertingDisableSendAlertsExternal experimental @grafana/alerting-squad false false false
153 preserveDashboardStateWhenNavigating experimental @grafana/dashboards-squad false false false
154 alertingCentralAlertHistory experimental @grafana/alerting-squad false false true

@ -607,10 +607,6 @@ const (
// Enables the new alert list view design
FlagAlertingListViewV2 = "alertingListViewV2"
// FlagDashboardRestore
// Enables deleted dashboard restore feature
FlagDashboardRestore = "dashboardRestore"
// FlagAlertingDisableSendAlertsExternal
// Disables the ability to send alerts to an external Alertmanager datasource.
FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal"

@ -1179,23 +1179,6 @@
"frontend": true
}
},
{
"metadata": {
"name": "dashboardRestore",
"resourceVersion": "1728397491294",
"creationTimestamp": "2024-05-16T17:36:26Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-10-08 14:24:51.294668 +0000 UTC"
}
},
"spec": {
"description": "Enables deleted dashboard restore feature",
"stage": "experimental",
"codeowner": "@grafana/search-and-storage",
"hideFromAdminPage": true,
"expression": "false"
}
},
{
"metadata": {
"name": "dashboardRestoreUI",

@ -1024,8 +1024,6 @@ func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folde
}
func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderCommand, folderUIDs []string) error {
// if dashboard restore is on we don't delete public dashboards, the hard delete will take care of it later
if !s.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
// We need a list of dashboard uids inside the folder to delete related public dashboards
dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: cmd.SignedInUser,
@ -1047,7 +1045,6 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm
if err != nil {
return folder.ErrInternal.Errorf("failed to delete public dashboards: %w", err)
}
}
// TODO use bulk delete
// Delete all dashboards in the folders

@ -196,7 +196,6 @@ func TestIntegrationFolderService(t *testing.T) {
})
t.Run("Given user has permission to save", func(t *testing.T) {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
service.features = featuremgmt.WithFeatures()
@ -299,61 +298,6 @@ func TestIntegrationFolderService(t *testing.T) {
require.Equal(t, orgID, actualCmd.OrgID)
require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules)
})
t.Run("When deleting folder by uid, expectedForceDeleteRules as false, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
f := folder.NewFolder(util.GenerateShortUID(), "")
f.UID = util.GenerateShortUID()
folderStore.On("Get", mock.Anything, mock.MatchedBy(func(query folder.GetFolderQuery) bool {
return query.OrgID == orgID && *query.UID == f.UID
})).Return(f, nil)
var actualCmd *dashboards.DeleteDashboardCommand
dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
actualCmd = args.Get(1).(*dashboards.DeleteDashboardCommand)
}).Return(nil).Once()
service.features = featuremgmt.WithFeatures(featuremgmt.FlagDashboardRestore)
expectedForceDeleteRules := false
err := service.Delete(context.Background(), &folder.DeleteFolderCommand{
UID: f.UID,
OrgID: orgID,
ForceDeleteRules: expectedForceDeleteRules,
SignedInUser: usr,
})
require.NoError(t, err)
require.NotNil(t, actualCmd)
require.Equal(t, orgID, actualCmd.OrgID)
require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules)
})
t.Run("When deleting folder by uid, expectedForceDeleteRules as true, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
f := folder.NewFolder(util.GenerateShortUID(), "")
f.UID = util.GenerateShortUID()
folderStore.On("Get", mock.Anything, mock.MatchedBy(func(query folder.GetFolderQuery) bool {
return query.OrgID == orgID && *query.UID == f.UID
})).Return(f, nil)
var actualCmd *dashboards.DeleteDashboardCommand
dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
actualCmd = args.Get(1).(*dashboards.DeleteDashboardCommand)
}).Return(nil).Once()
service.features = featuremgmt.WithFeatures(featuremgmt.FlagDashboardRestore)
expectedForceDeleteRules := true
err := service.Delete(context.Background(), &folder.DeleteFolderCommand{
UID: f.UID,
OrgID: orgID,
ForceDeleteRules: expectedForceDeleteRules,
SignedInUser: usr,
})
require.NoError(t, err)
require.NotNil(t, actualCmd)
require.Equal(t, orgID, actualCmd.OrgID)
require.Equal(t, expectedForceDeleteRules, actualCmd.ForceDeleteFolderRules)
})
t.Cleanup(func() {
service.features = featuremgmt.WithFeatures()
guardian.New = origNewGuardian
})
})
t.Run("Given user has permission to view", func(t *testing.T) {

@ -362,8 +362,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
require.NoError(t, err)
})
t.Run("When deleting folder by uid, expectedForceDeleteRules as false, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
folderService.features = featuremgmt.WithFeatures(append(featuresArr, featuremgmt.FlagDashboardRestore)...)
t.Run("When deleting folder by uid, expectedForceDeleteRules as false,should not return access denied error", func(t *testing.T) {
fakeK8sClient.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{Results: &resource.ResourceTable{}}, nil).Once()
expectedForceDeleteRules := false
@ -376,8 +375,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
require.NoError(t, err)
})
t.Run("When deleting folder by uid, expectedForceDeleteRules as true, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
folderService.features = featuremgmt.WithFeatures(append(featuresArr, featuremgmt.FlagDashboardRestore)...)
t.Run("When deleting folder by uid, expectedForceDeleteRules as true, should not return access denied error", func(t *testing.T) {
fakeK8sClient.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{Results: &resource.ResourceTable{}}, nil).Once()
expectedForceDeleteRules := true

@ -401,15 +401,6 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
Icon: "library-panel",
})
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestore) && (c.SignedInUser.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Recently deleted",
SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",
Id: "dashboards/recently-deleted",
Url: s.cfg.AppSubURL + "/dashboard/recently-deleted",
})
}
}
if hasAccess(ac.EvalPermission(dashboards.ActionDashboardsCreate)) {

@ -12,20 +12,11 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
@ -742,77 +733,3 @@ func TestDashboardIndex_MultiTermPrefixMatch(t *testing.T) {
})
}
}
func setupIntegrationEnv(t *testing.T, folderCount, dashboardsPerFolder int, sqlStore *sqlstore.SQLStore) (*StandardSearchService, *user.SignedInUser, error) {
err := populateDB(folderCount, dashboardsPerFolder, sqlStore)
require.NoError(t, err, "error when populating the database for integration test")
// load all dashboards and folders
dbLoadingBatchSize := (dashboardsPerFolder + 1) * folderCount
cfg := &setting.Cfg{Search: setting.SearchSettings{DashboardLoadingBatchSize: dbLoadingBatchSize}}
features := featuremgmt.WithFeatures()
orgSvc := &orgtest.FakeOrgService{
ExpectedOrgs: []*org.OrgDTO{{ID: 1}},
}
searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{},
tracing.InitializeTracerForTest(), features, orgSvc, nil, foldertest.NewFakeService()).(*StandardSearchService)
require.True(t, ok)
err = runSearchService(searchService)
require.NoError(t, err, "error when running search service for integration test")
user := getSignedInUser(folderCount, dashboardsPerFolder)
return searchService, user, nil
}
func TestIntegrationSoftDeletion(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
// Set up search v2.
folderCount := 1
dashboardsPerFolder := 1
sqlStore, cfg := db.InitTestDBWithCfg(t)
searchService, testUser, err := setupIntegrationEnv(t, folderCount, dashboardsPerFolder, sqlStore)
require.NoError(t, err)
// Query search v2 to ensure "dashboard2" is present.
result := searchService.doDashboardQuery(context.Background(), testUser, 1, DashboardQuery{Kind: []string{string(entityKindDashboard)}})
require.NoError(t, result.Error)
require.NotZero(t, len(result.Frames))
for _, field := range result.Frames[0].Fields {
if field.Name == "uid" {
require.Equal(t, dashboardsPerFolder, field.Len())
break
}
}
// Set up dashboard store.
featureToggles := featuremgmt.WithFeatures(
featuremgmt.FlagPanelTitleSearch,
featuremgmt.FlagDashboardRestore,
)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
// Soft delete "dashboard2".
err = dashboardStore.SoftDeleteDashboard(context.Background(), 1, "dashboard2")
require.NoError(t, err)
// Reindex to ensure "dashboard2" is excluded from the index.
searchService.dashboardIndex.reIndexFromScratch(context.Background())
// Query search v2 to ensure "dashboard2" is no longer present.
expectedResultCount := dashboardsPerFolder - 1
result2 := searchService.doDashboardQuery(context.Background(), testUser, 1, DashboardQuery{Kind: []string{string(entityKindDashboard)}})
require.NoError(t, result2.Error)
require.NotZero(t, len(result2.Frames))
for _, field := range result2.Frames[0].Fields {
if field.Name == "uid" {
require.Equal(t, expectedResultCount, field.Len())
break
}
}
}

@ -3539,84 +3539,6 @@
}
}
},
"/dashboards/uid/{uid}/trash": {
"delete": {
"description": "Will delete the dashboard given the specified unique identifier (uid).",
"tags": [
"dashboards"
],
"summary": "Hard delete dashboard by uid.",
"operationId": "hardDeleteDashboardByUID",
"parameters": [
{
"type": "string",
"name": "uid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/deleteDashboardResponse"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"403": {
"$ref": "#/responses/forbiddenError"
},
"404": {
"$ref": "#/responses/notFoundError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
},
"patch": {
"tags": [
"dashboards"
],
"summary": "Restore a dashboard to a given dashboard version using UID.",
"operationId": "restoreDeletedDashboardByUID",
"parameters": [
{
"type": "string",
"name": "uid",
"in": "path",
"required": true
},
{
"name": "Body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/RestoreDeletedDashboardCommand"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/postDashboardResponse"
},
"400": {
"$ref": "#/responses/badRequestError"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"403": {
"$ref": "#/responses/forbiddenError"
},
"404": {
"$ref": "#/responses/notFoundError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
}
},
"/dashboards/uid/{uid}/versions": {
"get": {
"tags": [

@ -4,7 +4,7 @@ import { useLocation, useParams } from 'react-router-dom-v5-compat';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { reportInteraction } from '@grafana/runtime';
import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { getConfig } from 'app/core/config';
@ -136,7 +136,8 @@ const BrowseDashboardsPage = memo(() => {
renderTitle={renderTitle}
actions={
<>
{config.featureToggles.dashboardRestore && hasAdminRights && (
{false &&
hasAdminRights && ( // TODO: change this to a feature flag when dashboard restore is reworked
<LinkButton
variant="secondary"
href={getConfig().appSubUrl + '/dashboard/recently-deleted'}

@ -293,7 +293,8 @@ export const browseDashboardsAPI = createApi({
// handling success alerts for these feature toggles
// for legacy response, the success alert will be triggered by showSuccessAlert function in public/app/core/services/backend_srv.ts
if (config.featureToggles.dashboardRestore) {
if (false) {
// TODO: change this to a feature flag when dashboard restore is reworked
const name = response?.title;
if (name) {

@ -115,6 +115,6 @@ function trackAction(action: keyof typeof actionMap, selectedItems: Omit<Dashboa
dashboard: selectedDashboards.length,
},
source: 'tree_actions',
restore_enabled: Boolean(config.featureToggles.dashboardRestore),
restore_enabled: false,
});
}

@ -1,6 +1,6 @@
import { useState } from 'react';
import { config, reportInteraction } from '@grafana/runtime';
import { reportInteraction } from '@grafana/runtime';
import { Alert, ConfirmModal, Text, Space } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
@ -27,7 +27,7 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
folder: Object.keys(selectedItems.folder).length,
},
source: 'browse_dashboards',
restore_enabled: Boolean(config.featureToggles.dashboardRestore),
restore_enabled: false,
});
setIsDeleting(true);
try {
@ -43,7 +43,7 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
<ConfirmModal
body={
<>
{config.featureToggles.dashboardRestore && (
{false && ( // TODO: change this to a feature flag when dashboard restore is reworked
<>
<Text element="p">
<Trans i18nKey="browse-dashboards.action.delete-modal-restore-dashboards-text">

@ -1,7 +1,7 @@
import * as React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { config, isFetchError } from '@grafana/runtime';
import { isFetchError } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { Alert, Box, Button, Stack } from '@grafana/ui';
@ -41,7 +41,7 @@ export interface NameAlreadyExistsErrorProps {
}
export function NameAlreadyExistsError({ cancelButton, saveButton }: NameAlreadyExistsErrorProps) {
const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore;
const isRestoreDashboardsEnabled = false;
return isRestoreDashboardsEnabled ? (
<Alert title={t('save-dashboards.name-exists.title', 'Dashboard name already exists')} severity="error">
<p>

@ -1,7 +1,7 @@
import { useAsyncFn, useToggle } from 'react-use';
import { selectors } from '@grafana/e2e-selectors';
import { config, reportInteraction } from '@grafana/runtime';
import { reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, Modal, Space, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
@ -34,7 +34,7 @@ export function DeleteDashboardButton({ dashboard }: ButtonProps) {
dashboard: 1,
},
source: 'dashboard_scene_settings',
restore_enabled: Boolean(config.featureToggles.dashboardRestore),
restore_enabled: false,
});
toggleModal();
if (dashboard.state.uid) {
@ -81,7 +81,7 @@ export function DeleteDashboardModal({ dashboardTitle, onConfirm, onClose }: Del
isOpen={true}
body={
<>
{config.featureToggles.dashboardRestore && (
{false && ( // TODO: re-enable when restore is reworked
<>
<Text element="p">
<Trans i18nKey="dashboard-settings.delete-modal-restore-dashboards-text">

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { connect, ConnectedProps } from 'react-redux';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { locationService, config, reportInteraction } from '@grafana/runtime';
import { locationService, reportInteraction } from '@grafana/runtime';
import { Modal, Button, Text, Space, TextLink } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { cleanUpDashboardAndVariables } from 'app/features/dashboard/state/actions';
@ -34,7 +34,7 @@ const DeleteDashboardModalUnconnected = ({ hideModal, cleanUpDashboardAndVariabl
dashboard: 1,
},
source: 'dashboard_settings',
restore_enabled: Boolean(config.featureToggles.dashboardRestore),
restore_enabled: false,
});
await deleteItems({
selectedItems: {

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config, FetchError } from '@grafana/runtime';
import { FetchError } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { Button, ConfirmModal, Modal, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
@ -31,7 +31,7 @@ export const SaveDashboardErrorProxy = ({
setErrorIsHandled,
}: SaveDashboardErrorProxyProps) => {
const { onDashboardSave } = useDashboardSave();
const isRestoreDashboardsEnabled = config.featureToggles.dashboardRestore;
const isRestoreDashboardsEnabled = false;
return (
<>
{error.data && error.data.status === 'version-mismatch' && (

@ -449,13 +449,6 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "SnapshotListPage" */ 'app/features/manage-dashboards/SnapshotListPage')
),
},
config.featureToggles.dashboardRestore && {
path: '/dashboard/recently-deleted',
roles: () => ['Admin', 'ServerAdmin'],
component: SafeDynamicImport(
() => import(/* webpackChunkName: "RecentlyDeletedPage" */ 'app/features/browse-dashboards/RecentlyDeletedPage')
),
},
{
path: '/playlists',
component: SafeDynamicImport(

@ -17182,91 +17182,6 @@
]
}
},
"/dashboards/uid/{uid}/trash": {
"delete": {
"description": "Will delete the dashboard given the specified unique identifier (uid).",
"operationId": "hardDeleteDashboardByUID",
"parameters": [
{
"in": "path",
"name": "uid",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/deleteDashboardResponse"
},
"401": {
"$ref": "#/components/responses/unauthorisedError"
},
"403": {
"$ref": "#/components/responses/forbiddenError"
},
"404": {
"$ref": "#/components/responses/notFoundError"
},
"500": {
"$ref": "#/components/responses/internalServerError"
}
},
"summary": "Hard delete dashboard by uid.",
"tags": [
"dashboards"
]
},
"patch": {
"operationId": "restoreDeletedDashboardByUID",
"parameters": [
{
"in": "path",
"name": "uid",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RestoreDeletedDashboardCommand"
}
}
},
"required": true,
"x-originalParamName": "Body"
},
"responses": {
"200": {
"$ref": "#/components/responses/postDashboardResponse"
},
"400": {
"$ref": "#/components/responses/badRequestError"
},
"401": {
"$ref": "#/components/responses/unauthorisedError"
},
"403": {
"$ref": "#/components/responses/forbiddenError"
},
"404": {
"$ref": "#/components/responses/notFoundError"
},
"500": {
"$ref": "#/components/responses/internalServerError"
}
},
"summary": "Restore a dashboard to a given dashboard version using UID.",
"tags": [
"dashboards"
]
}
},
"/dashboards/uid/{uid}/versions": {
"get": {
"operationId": "getDashboardVersionsByUID",

Loading…
Cancel
Save