mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FindDashboards: filter by dashboard type (#100160)
This commit is contained in:
parent
f7d476e408
commit
495aa65c6e
@ -46,6 +46,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/services/search/model"
|
"github.com/grafana/grafana/pkg/services/search/model"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -232,7 +233,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context,
|
|||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
func(orgID int64) {
|
func(orgID int64) {
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
ProvisionedRepo: name,
|
ProvisionedRepo: name,
|
||||||
OrgId: orgID,
|
OrgId: orgID,
|
||||||
})
|
})
|
||||||
@ -273,7 +274,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx con
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
OrgId: org.ID,
|
OrgId: org.ID,
|
||||||
DashboardIds: []int64{dashboardID},
|
DashboardIds: []int64{dashboardID},
|
||||||
})
|
})
|
||||||
@ -300,7 +301,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
OrgId: orgID,
|
OrgId: orgID,
|
||||||
DashboardUIDs: []string{dashboardUID},
|
DashboardUIDs: []string{dashboardUID},
|
||||||
})
|
})
|
||||||
@ -559,7 +560,7 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.
|
|||||||
for _, org := range orgs {
|
for _, org := range orgs {
|
||||||
ctx, _ := identity.WithServiceIdentity(ctx, org.ID)
|
ctx, _ := identity.WithServiceIdentity(ctx, org.ID)
|
||||||
// find all dashboards in the org that have a file repo set that is not in the given readers list
|
// find all dashboards in the org that have a file repo set that is not in the given readers list
|
||||||
foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
ProvisionedReposNotIn: cmd.ReaderNames,
|
ProvisionedReposNotIn: cmd.ReaderNames,
|
||||||
OrgId: org.ID,
|
OrgId: org.ID,
|
||||||
})
|
})
|
||||||
@ -1436,7 +1437,12 @@ func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We need a list of dashboard uids inside the folder to delete related public dashboards
|
// 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, FolderUIDs: folderUIDs, OrgId: orgID})
|
dashes, err := dr.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
SignedInUser: u,
|
||||||
|
FolderUIDs: folderUIDs,
|
||||||
|
OrgId: orgID,
|
||||||
|
Type: searchstore.TypeDashboard,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||||
}
|
}
|
||||||
@ -1729,7 +1735,11 @@ type dashboardProvisioningWithUID struct {
|
|||||||
DashboardUID string
|
DashboardUID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]*dashboardProvisioningWithUID, error) {
|
func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboardProvisioningWithUID, error) {
|
||||||
|
if query == nil {
|
||||||
|
return nil, errors.New("query cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
ctx, _ = identity.WithServiceIdentity(ctx, query.OrgId)
|
ctx, _ = identity.WithServiceIdentity(ctx, query.OrgId)
|
||||||
|
|
||||||
if query.ProvisionedRepo != "" {
|
if query.ProvisionedRepo != "" {
|
||||||
@ -1744,7 +1754,9 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
|
|||||||
query.ProvisionedReposNotIn = repos
|
query.ProvisionedReposNotIn = repos
|
||||||
}
|
}
|
||||||
|
|
||||||
searchResults, err := dr.searchDashboardsThroughK8sRaw(ctx, &query)
|
query.Type = searchstore.TypeDashboard
|
||||||
|
|
||||||
|
searchResults, err := dr.searchDashboardsThroughK8sRaw(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1807,6 +1819,11 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboards.Dashboard, error) {
|
func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboards.Dashboard, error) {
|
||||||
|
if query == nil {
|
||||||
|
return nil, errors.New("query cannot be nil")
|
||||||
|
}
|
||||||
|
query.Type = searchstore.TypeDashboard
|
||||||
|
|
||||||
response, err := dr.searchDashboardsThroughK8sRaw(ctx, query)
|
response, err := dr.searchDashboardsThroughK8sRaw(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1899,6 +1899,134 @@ func TestCountInFolders(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchDashboardsThroughK8sRaw(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
k8sCliMock := new(client.MockK8sHandler)
|
||||||
|
service := &DashboardServiceImpl{k8sclient: k8sCliMock}
|
||||||
|
query := &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
OrgId: 1,
|
||||||
|
}
|
||||||
|
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
|
||||||
|
Results: &resource.ResourceTable{
|
||||||
|
Columns: []*resource.ResourceTableColumnDefinition{
|
||||||
|
{
|
||||||
|
Name: "title",
|
||||||
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "folder",
|
||||||
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rows: []*resource.ResourceTableRow{
|
||||||
|
{
|
||||||
|
Key: &resource.ResourceKey{
|
||||||
|
Name: "uid",
|
||||||
|
Resource: "dashboard",
|
||||||
|
},
|
||||||
|
Cells: [][]byte{
|
||||||
|
[]byte("Dashboard 1"),
|
||||||
|
[]byte("folder1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TotalHits: 1,
|
||||||
|
}, nil)
|
||||||
|
res, err := service.searchDashboardsThroughK8s(ctx, query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []*dashboards.Dashboard{
|
||||||
|
{
|
||||||
|
UID: "uid",
|
||||||
|
OrgID: 1,
|
||||||
|
FolderUID: "folder1",
|
||||||
|
Title: "Dashboard 1",
|
||||||
|
Slug: "dashboard-1", // should be slugified
|
||||||
|
},
|
||||||
|
}, res)
|
||||||
|
assert.Equal(t, "dash-db", query.Type) // query type should be added
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
k8sCliMock := new(client.MockK8sHandler)
|
||||||
|
service := &DashboardServiceImpl{k8sclient: k8sCliMock}
|
||||||
|
query := &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
OrgId: 1,
|
||||||
|
}
|
||||||
|
dashboardUnstructuredProvisioned := unstructured.Unstructured{Object: map[string]any{
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"name": "uid",
|
||||||
|
"annotations": map[string]any{
|
||||||
|
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
|
||||||
|
utils.AnnoKeyRepoHash: "hash",
|
||||||
|
utils.AnnoKeyRepoPath: "path/to/file",
|
||||||
|
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"spec": map[string]any{},
|
||||||
|
}}
|
||||||
|
dashboardUnstructuredNotProvisioned := unstructured.Unstructured{Object: map[string]any{
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"name": "uid2",
|
||||||
|
},
|
||||||
|
"spec": map[string]any{},
|
||||||
|
}}
|
||||||
|
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
|
||||||
|
Results: &resource.ResourceTable{
|
||||||
|
Columns: []*resource.ResourceTableColumnDefinition{
|
||||||
|
{
|
||||||
|
Name: "title",
|
||||||
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "folder",
|
||||||
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rows: []*resource.ResourceTableRow{
|
||||||
|
{
|
||||||
|
Key: &resource.ResourceKey{
|
||||||
|
Name: "uid",
|
||||||
|
Resource: "dashboard",
|
||||||
|
},
|
||||||
|
Cells: [][]byte{
|
||||||
|
[]byte("Dashboard 1"),
|
||||||
|
[]byte("folder1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: &resource.ResourceKey{
|
||||||
|
Name: "uid2",
|
||||||
|
Resource: "dashboard",
|
||||||
|
},
|
||||||
|
Cells: [][]byte{
|
||||||
|
[]byte("Dashboard 2"),
|
||||||
|
[]byte("folder2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TotalHits: 1,
|
||||||
|
}, nil)
|
||||||
|
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructuredProvisioned, nil).Once()
|
||||||
|
k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructuredNotProvisioned, nil).Once()
|
||||||
|
res, err := service.searchProvisionedDashboardsThroughK8s(ctx, query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []*dashboardProvisioningWithUID{
|
||||||
|
{
|
||||||
|
DashboardUID: "uid",
|
||||||
|
DashboardProvisioning: dashboards.DashboardProvisioning{
|
||||||
|
Name: "test",
|
||||||
|
ExternalID: "path/to/file",
|
||||||
|
CheckSum: "hash",
|
||||||
|
Updated: 1735689600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, res) // only should return the one provisioned dashboard
|
||||||
|
assert.Equal(t, "dash-db", query.Type) // query type should be added as dashboards only
|
||||||
|
}
|
||||||
|
|
||||||
func TestLegacySaveCommandToUnstructured(t *testing.T) {
|
func TestLegacySaveCommandToUnstructured(t *testing.T) {
|
||||||
namespace := "test-namespace"
|
namespace := "test-namespace"
|
||||||
t.Run("successfully converts save command to unstructured", func(t *testing.T) {
|
t.Run("successfully converts save command to unstructured", func(t *testing.T) {
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/search/model"
|
"github.com/grafana/grafana/pkg/services/search/model"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||||
"github.com/grafana/grafana/pkg/services/supportbundles"
|
"github.com/grafana/grafana/pkg/services/supportbundles"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@ -1028,7 +1029,12 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm
|
|||||||
// if dashboard restore is on we don't delete public dashboards, the hard delete will take care of it later
|
// 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) {
|
if !s.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
||||||
// We need a list of dashboard uids inside the folder to delete related public dashboards
|
// 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, FolderUIDs: folderUIDs, OrgId: cmd.OrgID})
|
dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
SignedInUser: cmd.SignedInUser,
|
||||||
|
FolderUIDs: folderUIDs,
|
||||||
|
OrgId: cmd.OrgID,
|
||||||
|
Type: searchstore.TypeDashboard,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/search/model"
|
"github.com/grafana/grafana/pkg/services/search/model"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
@ -737,7 +738,12 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{SignedInUser: cmd.SignedInUser, FolderUIDs: folders, OrgId: cmd.OrgID})
|
dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
SignedInUser: cmd.SignedInUser,
|
||||||
|
FolderUIDs: folders,
|
||||||
|
OrgId: cmd.OrgID,
|
||||||
|
Type: searchstore.TypeDashboard,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/search"
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
@ -252,6 +253,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn
|
|||||||
// then find the dashboards that were supposed to be connected to this element
|
// then find the dashboards that were supposed to be connected to this element
|
||||||
_, requester := identity.WithServiceIdentity(c, signedInUser.GetOrgID())
|
_, requester := identity.WithServiceIdentity(c, signedInUser.GetOrgID())
|
||||||
dashs, err := l.dashboardsService.FindDashboards(c, &dashboards.FindPersistedDashboardsQuery{
|
dashs, err := l.dashboardsService.FindDashboards(c, &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
Type: searchstore.TypeDashboard,
|
||||||
OrgId: signedInUser.GetOrgID(),
|
OrgId: signedInUser.GetOrgID(),
|
||||||
DashboardIds: dashboardIDs,
|
DashboardIds: dashboardIDs,
|
||||||
SignedInUser: requester, // a user may be able to delete a library element but not read all dashboards. We still need to run this check, so we don't allow deleting elements if dashboards are connected
|
SignedInUser: requester, // a user may be able to delete a library element but not read all dashboards. We still need to run this check, so we don't allow deleting elements if dashboards are connected
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2"
|
"github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2"
|
||||||
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
||||||
"github.com/grafana/grafana/pkg/services/query"
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@ -375,7 +376,13 @@ func (pd *PublicDashboardServiceImpl) FindAllWithPagination(ctx context.Context,
|
|||||||
dashUIDs[i] = pubdash.DashboardUid
|
dashUIDs[i] = pubdash.DashboardUid
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardsFound, err := pd.dashboardService.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: query.OrgID, DashboardUIDs: dashUIDs, SignedInUser: query.User, Limit: int64(len(dashUIDs))})
|
dashboardsFound, err := pd.dashboardService.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
OrgId: query.OrgID,
|
||||||
|
DashboardUIDs: dashUIDs,
|
||||||
|
SignedInUser: query.User,
|
||||||
|
Limit: int64(len(dashUIDs)),
|
||||||
|
Type: searchstore.TypeDashboard,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInternalServerError.Errorf("FindAllWithPagination: GetDashboards: %w", err)
|
return nil, ErrInternalServerError.Errorf("FindAllWithPagination: GetDashboards: %w", err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user