mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudMigrations: Add support for migration of plugin resources (#95612)
* start plugins migration * more plugin work * add warning * fakepluginsettings test * tests get plugins * lint * load logos * go lint * get all plugins once * locales * josh suggestion to inject query in rtk * more plugin filters * remove datasource warning * access control for plugins * remove unused method * lint * use gcom list
This commit is contained in:
parent
6ca6ad4df7
commit
1699dfa307
2
go.mod
2
go.mod
@ -86,7 +86,7 @@ require (
|
||||
github.com/grafana/grafana-app-sdk v0.23.1 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5 // @grafana/aws-datasources
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 // @grafana/partner-datasources
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.3 // @grafana/plugins-platform-backend
|
||||
|
4
go.sum
4
go.sum
@ -2323,8 +2323,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGAnI27HhQkaLttzbPE=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 h1:F0O9eTy4jHjEd1Z3/qIza2GdY7PYpTddUeaq9p3NKGU=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0/go.mod h1:bd6Cm06EK0MzRO5ahUpbDz1SxNOKu+fzladbaRPHZPY=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 h1:S4kHwr//AqhtL9xHBtz1gqVgZQeCRGTxjgsRBAkpjKY=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0/go.mod h1:rWNhyxYkgiXgV7xZ4yOQzMV08yikO8L8S8M5KNoQNpA=
|
||||
github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA=
|
||||
github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
||||
|
@ -131,6 +131,7 @@ const (
|
||||
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
|
||||
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
|
||||
MuteTimingType MigrateDataType = "MUTE_TIMING"
|
||||
PluginDataType MigrateDataType = "PLUGIN"
|
||||
)
|
||||
|
||||
// swagger:enum ItemStatus
|
||||
@ -158,7 +159,6 @@ const (
|
||||
ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT"
|
||||
ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE"
|
||||
ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR"
|
||||
ErrOnlyCoreDataSources ItemErrorCode = "ONLY_CORE_DATA_SOURCES"
|
||||
ErrGeneric ItemErrorCode = "GENERIC_ERROR"
|
||||
)
|
||||
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authapi"
|
||||
"github.com/grafana/grafana/pkg/services/authapi/fake"
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
@ -32,6 +33,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/gcom"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||
@ -68,6 +70,8 @@ type Service struct {
|
||||
dashboardService dashboards.DashboardService
|
||||
folderService folder.Service
|
||||
pluginStore pluginstore.Store
|
||||
accessControl accesscontrol.AccessControl
|
||||
pluginSettingsService pluginsettings.Service
|
||||
secretsService secrets.Service
|
||||
kvStore *kvstore.NamespacedKVStore
|
||||
libraryElementsService libraryelements.Service
|
||||
@ -105,6 +109,8 @@ func ProvideService(
|
||||
dashboardService dashboards.DashboardService,
|
||||
folderService folder.Service,
|
||||
pluginStore pluginstore.Store,
|
||||
pluginSettingsService pluginsettings.Service,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
kvStore kvstore.KVStore,
|
||||
libraryElementsService libraryelements.Service,
|
||||
ngAlert *ngalert.AlertNG,
|
||||
@ -125,6 +131,8 @@ func ProvideService(
|
||||
dashboardService: dashboardService,
|
||||
folderService: folderService,
|
||||
pluginStore: pluginStore,
|
||||
pluginSettingsService: pluginSettingsService,
|
||||
accessControl: accessControl,
|
||||
kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"),
|
||||
libraryElementsService: libraryElementsService,
|
||||
ngAlert: ngAlert,
|
||||
@ -588,13 +596,7 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps
|
||||
s.log.Error("unexpected GMS snapshot state: %s", snapshotMeta.State)
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
// For 11.2 we only support core data sources. Apply a warning for any non-core ones before storing.
|
||||
resources, err := s.getResourcesWithPluginWarnings(ctx, snapshotMeta.Results)
|
||||
if err != nil {
|
||||
// treat this as non-fatal since the migration still succeeded
|
||||
s.log.Error("error applying plugin warnings, please open a bug report: %w", err)
|
||||
}
|
||||
resources := snapshotMeta.Results
|
||||
|
||||
// Log the errors for resources with errors at migration
|
||||
for _, resource := range resources {
|
||||
@ -901,40 +903,3 @@ func (s *Service) deleteLocalFiles(snapshots []cloudmigration.CloudMigrationSnap
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// getResourcesWithPluginWarnings iterates through each resource and, if a non-core datasource, applies a warning that we only support core
|
||||
func (s *Service) getResourcesWithPluginWarnings(ctx context.Context, results []cloudmigration.CloudMigrationResource) ([]cloudmigration.CloudMigrationResource, error) {
|
||||
dsList, err := s.dsService.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting all data sources: %w", err)
|
||||
}
|
||||
dsMap := make(map[string]*datasources.DataSource, len(dsList))
|
||||
for i := 0; i < len(dsList); i++ {
|
||||
dsMap[dsList[i].UID] = dsList[i]
|
||||
}
|
||||
|
||||
for i := 0; i < len(results); i++ {
|
||||
r := results[i]
|
||||
|
||||
if r.Type == cloudmigration.DatasourceDataType &&
|
||||
r.Error == "" { // any error returned by GMS takes priority
|
||||
ds, ok := dsMap[r.RefID]
|
||||
if !ok {
|
||||
s.log.Error("data source with id %s was not found in data sources list", r.RefID)
|
||||
continue
|
||||
}
|
||||
|
||||
p, found := s.pluginStore.Plugin(ctx, ds.Type)
|
||||
// if the plugin is not found, it means it was uninstalled, meaning it wasn't core
|
||||
if !p.IsCorePlugin() || !found {
|
||||
r.Status = cloudmigration.ItemStatusWarning
|
||||
r.ErrorCode = cloudmigration.ErrOnlyCoreDataSources
|
||||
r.Error = "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack."
|
||||
}
|
||||
|
||||
results[i] = r
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
@ -24,8 +24,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration/gmsclient"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||
@ -39,6 +37,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
@ -447,146 +446,6 @@ func Test_SortFolders(t *testing.T) {
|
||||
require.Equal(t, expected, sortedFolders)
|
||||
}
|
||||
|
||||
func Test_NonCoreDataSourcesHaveWarning(t *testing.T) {
|
||||
s := setUpServiceTest(t, false).(*Service)
|
||||
|
||||
// Insert a processing snapshot into the database before we start so we query GMS
|
||||
createTokenResp, err := s.CreateToken(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, createTokenResp.Token)
|
||||
|
||||
sess, err := s.store.CreateMigrationSession(context.Background(), cloudmigration.CloudMigrationSession{
|
||||
AuthToken: createTokenResp.Token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
snapshotUid, err := s.store.CreateSnapshot(context.Background(), cloudmigration.CloudMigrationSnapshot{
|
||||
UID: uuid.NewString(),
|
||||
SessionUID: sess.UID,
|
||||
Status: cloudmigration.SnapshotStatusProcessing,
|
||||
GMSSnapshotUID: "gms uid",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// GMS should return: a core ds, a non-core ds, a non-core ds with an error, and a ds that has been uninstalled
|
||||
gmsClientMock := &gmsClientMock{
|
||||
getSnapshotResponse: &cloudmigration.GetSnapshotStatusResponse{
|
||||
State: cloudmigration.SnapshotStateFinished,
|
||||
Results: []cloudmigration.CloudMigrationResource{
|
||||
{
|
||||
Name: "1 name",
|
||||
ParentName: "1 parent name",
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: "1", // this will be core
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
SnapshotUID: snapshotUid,
|
||||
},
|
||||
{
|
||||
Name: "2 name",
|
||||
ParentName: "",
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: "2", // this will be non-core
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
SnapshotUID: snapshotUid,
|
||||
},
|
||||
{
|
||||
Name: "3 name",
|
||||
ParentName: "3 parent name",
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: "3", // this will be non-core with an error
|
||||
Status: cloudmigration.ItemStatusError,
|
||||
Error: "please don't overwrite me",
|
||||
SnapshotUID: snapshotUid,
|
||||
},
|
||||
{
|
||||
Name: "4 name",
|
||||
ParentName: "4 folder name",
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: "4", // this will be deleted
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
SnapshotUID: snapshotUid,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
s.gmsClient = gmsClientMock
|
||||
|
||||
// Update the internal plugin store and ds store with seed data matching the descriptions above
|
||||
s.pluginStore = pluginstore.NewFakePluginStore([]pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "1",
|
||||
},
|
||||
Class: plugins.ClassCore,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "2",
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "3",
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
},
|
||||
}...)
|
||||
|
||||
s.dsService = &datafakes.FakeDataSourceService{
|
||||
DataSources: []*datasources.DataSource{
|
||||
{UID: "1", Type: "1"},
|
||||
{UID: "2", Type: "2"},
|
||||
{UID: "3", Type: "3"},
|
||||
{UID: "4", Type: "4"},
|
||||
},
|
||||
}
|
||||
|
||||
var snapshot *cloudmigration.CloudMigrationSnapshot
|
||||
hasFourResources := func() bool {
|
||||
// Retrieve the snapshot with results
|
||||
var err error
|
||||
snapshot, err = s.GetSnapshot(ctxWithSignedInUser(), cloudmigration.GetSnapshotsQuery{
|
||||
SnapshotUID: snapshotUid,
|
||||
SessionUID: sess.UID,
|
||||
ResultPage: 1,
|
||||
ResultLimit: 10,
|
||||
})
|
||||
|
||||
if !assert.NoError(t, err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(snapshot.Resources) == 4
|
||||
}
|
||||
|
||||
require.Eventually(t, hasFourResources, time.Second, 10*time.Millisecond)
|
||||
|
||||
findRef := func(id string) *cloudmigration.CloudMigrationResource {
|
||||
for _, r := range snapshot.Resources {
|
||||
if r.RefID == id {
|
||||
return &r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
shouldBeUnaltered := findRef("1")
|
||||
assert.Equal(t, cloudmigration.ItemStatusOK, shouldBeUnaltered.Status)
|
||||
assert.Empty(t, shouldBeUnaltered.Error)
|
||||
|
||||
shouldBeAltered := findRef("2")
|
||||
assert.Equal(t, cloudmigration.ItemStatusWarning, shouldBeAltered.Status)
|
||||
assert.Equal(t, shouldBeAltered.Error, "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.")
|
||||
|
||||
shouldHaveOriginalError := findRef("3")
|
||||
assert.Equal(t, cloudmigration.ItemStatusError, shouldHaveOriginalError.Status)
|
||||
assert.Equal(t, shouldHaveOriginalError.Error, "please don't overwrite me")
|
||||
|
||||
uninstalledAltered := findRef("4")
|
||||
assert.Equal(t, cloudmigration.ItemStatusWarning, uninstalledAltered.Status)
|
||||
assert.Equal(t, uninstalledAltered.Error, "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.")
|
||||
}
|
||||
|
||||
func TestDeleteSession(t *testing.T) {
|
||||
s := setUpServiceTest(t, false).(*Service)
|
||||
user := &user.SignedInUser{UserUID: "user123"}
|
||||
@ -817,13 +676,135 @@ func TestGetLibraryElementsCommands(t *testing.T) {
|
||||
require.Equal(t, createLibraryElementCmd.UID, cmds[0].UID)
|
||||
}
|
||||
|
||||
func ctxWithSignedInUser() context.Context {
|
||||
c := &contextmodel.ReqContext{
|
||||
SignedInUser: &user.SignedInUser{OrgID: 1},
|
||||
// NOTE: this should be on the plugin object
|
||||
func TestIsPublicSignatureType(t *testing.T) {
|
||||
testcases := []struct {
|
||||
signature plugins.SignatureType
|
||||
expectedPublic bool
|
||||
}{
|
||||
{
|
||||
signature: plugins.SignatureTypeCommunity,
|
||||
expectedPublic: true,
|
||||
},
|
||||
{
|
||||
signature: plugins.SignatureTypeCommercial,
|
||||
expectedPublic: true,
|
||||
},
|
||||
{
|
||||
signature: plugins.SignatureTypeGrafana,
|
||||
expectedPublic: true,
|
||||
},
|
||||
{
|
||||
signature: plugins.SignatureTypePrivate,
|
||||
expectedPublic: false,
|
||||
},
|
||||
{
|
||||
signature: plugins.SignatureTypePrivateGlob,
|
||||
expectedPublic: false,
|
||||
},
|
||||
}
|
||||
k := ctxkey.Key{}
|
||||
ctx := context.WithValue(context.Background(), k, c)
|
||||
return ctx
|
||||
|
||||
for _, testcase := range testcases {
|
||||
resPublic := IsPublicSignatureType(testcase.signature)
|
||||
require.Equal(t, resPublic, testcase.expectedPublic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPlugins(t *testing.T) {
|
||||
s := setUpServiceTest(t, false).(*Service)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
user := &user.SignedInUser{OrgID: 1}
|
||||
|
||||
s.pluginStore = pluginstore.NewFakePluginStore([]pluginstore.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "plugin-core",
|
||||
Type: plugins.TypeDataSource,
|
||||
},
|
||||
Class: plugins.ClassCore,
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "plugin-external-valid-grafana",
|
||||
Type: plugins.TypeDataSource,
|
||||
AutoEnabled: false,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "plugin-external-valid-commercial",
|
||||
Type: plugins.TypePanel,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeCommercial,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "plugin-external-valid-community",
|
||||
Type: plugins.TypePanel,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeCommunity,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "plugin-external-invalid",
|
||||
Type: plugins.TypePanel,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Signature: plugins.SignatureStatusInvalid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "plugin-external-unsigned",
|
||||
Type: plugins.TypePanel,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Signature: plugins.SignatureStatusUnsigned,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
},
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "plugin-external-valid-private",
|
||||
Type: plugins.TypeApp,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Signature: plugins.SignatureStatusUnsigned,
|
||||
SignatureType: plugins.SignatureTypePrivate,
|
||||
},
|
||||
}...)
|
||||
|
||||
s.pluginSettingsService = &pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
||||
"plugin-external-valid-grafana": {ID: 0, OrgID: user.OrgID, PluginID: "plugin-external-valid-grafana", PluginVersion: "1.0.0", Enabled: true},
|
||||
}}
|
||||
|
||||
plugins, err := s.getPlugins(ctx, user)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, plugins)
|
||||
require.Len(t, plugins, 3)
|
||||
|
||||
expectedPluginIDs := []string{"plugin-external-valid-grafana", "plugin-external-valid-commercial", "plugin-external-valid-community"}
|
||||
pluginsIDs := make([]string, 0)
|
||||
for _, plugin := range plugins {
|
||||
// Special case of using the settings from the settings store
|
||||
if plugin.ID == "plugin-external-valid-grafana" {
|
||||
require.True(t, plugin.SettingCmd.Enabled)
|
||||
}
|
||||
|
||||
pluginsIDs = append(pluginsIDs, plugin.ID)
|
||||
}
|
||||
require.ElementsMatch(t, pluginsIDs, expectedPluginIDs)
|
||||
}
|
||||
|
||||
type configOverrides func(c *setting.Cfg)
|
||||
@ -949,6 +930,8 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf
|
||||
dashboardService,
|
||||
mockFolder,
|
||||
&pluginstore.FakePluginStore{},
|
||||
&pluginsettings.FakePluginSettings{},
|
||||
actest.FakeAccessControl{ExpectedEvaluate: true},
|
||||
kvstore.ProvideService(sqlStore),
|
||||
&libraryelementsfake.LibraryElementService{},
|
||||
ng,
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
cryptoRand "crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -14,12 +15,18 @@ import (
|
||||
snapshot "github.com/grafana/grafana-cloud-migration-snapshot/src"
|
||||
"github.com/grafana/grafana-cloud-migration-snapshot/src/contracts"
|
||||
"github.com/grafana/grafana-cloud-migration-snapshot/src/infra/crypto"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
plugins "github.com/grafana/grafana/pkg/plugins"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/util/retryer"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
@ -37,12 +44,20 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{
|
||||
cloudmigration.ContactPointType,
|
||||
cloudmigration.NotificationPolicyType,
|
||||
cloudmigration.AlertRuleType,
|
||||
cloudmigration.PluginDataType,
|
||||
}
|
||||
|
||||
func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getMigrationDataJSON")
|
||||
defer span.End()
|
||||
|
||||
// Plugins
|
||||
plugins, err := s.getPlugins(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get plugins", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Data sources
|
||||
dataSources, err := s.getDataSourceCommands(ctx, signedInUser)
|
||||
if err != nil {
|
||||
@ -100,10 +115,19 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S
|
||||
|
||||
migrationDataSlice := make(
|
||||
[]cloudmigration.MigrateDataRequestItem, 0,
|
||||
len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+
|
||||
len(plugins)+len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+
|
||||
len(muteTimings)+len(notificationTemplates)+len(contactPoints)+len(alertRules),
|
||||
)
|
||||
|
||||
for _, plugin := range plugins {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.PluginDataType,
|
||||
RefID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Data: plugin.SettingCmd,
|
||||
})
|
||||
}
|
||||
|
||||
for _, ds := range dataSources {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
@ -356,6 +380,105 @@ func (s *Service) getLibraryElementsCommands(ctx context.Context, signedInUser *
|
||||
return cmds, nil
|
||||
}
|
||||
|
||||
type PluginCmd struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SettingCmd pluginsettings.UpdatePluginSettingCmd `json:"settingCmd"`
|
||||
}
|
||||
|
||||
// IsPublicSignatureType returns true if plugin signature type is public
|
||||
func IsPublicSignatureType(signatureType plugins.SignatureType) bool {
|
||||
switch signatureType {
|
||||
case plugins.SignatureTypeGrafana, plugins.SignatureTypeCommercial, plugins.SignatureTypeCommunity:
|
||||
return true
|
||||
case plugins.SignatureTypePrivate, plugins.SignatureTypePrivateGlob:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getPlugins returns the json payloads required by the plugin creation API
|
||||
func (s *Service) getPlugins(ctx context.Context, signedInUser *user.SignedInUser) ([]PluginCmd, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getPlugins")
|
||||
defer span.End()
|
||||
|
||||
results := make([]PluginCmd, 0)
|
||||
plugins := s.pluginStore.Plugins(ctx)
|
||||
|
||||
// Obtain plugins from gcom
|
||||
requestID := tracing.TraceIDFromContext(ctx, false)
|
||||
gcomPlugins, err := s.gcomService.GetPlugins(ctx, requestID)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("fetching gcom plugins: %w", err)
|
||||
}
|
||||
|
||||
// Permissions for listing plugins, taken from plugins api
|
||||
userIsOrgAdmin := signedInUser.HasRole(org.RoleAdmin)
|
||||
hasAccess, _ := s.accessControl.Evaluate(ctx, signedInUser, ac.EvalAny(
|
||||
ac.EvalPermission(datasources.ActionCreate),
|
||||
ac.EvalPermission(pluginaccesscontrol.ActionInstall),
|
||||
))
|
||||
if !(userIsOrgAdmin || hasAccess) {
|
||||
s.log.Info("user is not allowed to list non-core plugins", "UID", signedInUser.UserUID)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
// filter plugins to keep only the ones allowed by gcom
|
||||
if _, exists := gcomPlugins[plugin.ID]; !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// filter plugins to keep only non core, signed, with public signature type plugins
|
||||
if plugin.IsCorePlugin() || !plugin.Signature.IsValid() || !IsPublicSignatureType(plugin.SignatureType) {
|
||||
continue
|
||||
}
|
||||
// filter out dependent app plugins
|
||||
if plugin.IncludedInAppID != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Permissions filtering, taken from plugins api
|
||||
hasAccess, _ = s.accessControl.Evaluate(ctx, signedInUser, ac.EvalPermission(pluginaccesscontrol.ActionWrite, pluginaccesscontrol.ScopeProvider.GetResourceScope(plugin.ID)))
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginSettingCmd := pluginsettings.UpdatePluginSettingCmd{
|
||||
Enabled: plugin.JSONData.AutoEnabled,
|
||||
Pinned: plugin.Pinned,
|
||||
PluginVersion: plugin.Info.Version,
|
||||
PluginId: plugin.ID,
|
||||
}
|
||||
|
||||
// get plugin settings from db if they exist
|
||||
ps, err := s.pluginSettingsService.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{
|
||||
PluginID: plugin.ID,
|
||||
OrgID: signedInUser.OrgID,
|
||||
})
|
||||
if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) {
|
||||
return nil, fmt.Errorf("failed to get plugin settings: %w", err)
|
||||
} else if ps != nil {
|
||||
pluginSettingCmd.Enabled = ps.Enabled
|
||||
pluginSettingCmd.Pinned = ps.Pinned
|
||||
pluginSettingCmd.JsonData = ps.JSONData
|
||||
decryptedData, err := s.secretsService.DecryptJsonData(ctx, ps.SecureJSONData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt secure json data: %w", err)
|
||||
}
|
||||
pluginSettingCmd.SecureJsonData = decryptedData
|
||||
}
|
||||
|
||||
results = append(results, PluginCmd{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
SettingCmd: pluginSettingCmd,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// asynchronous process for writing the snapshot to the filesystem and updating the snapshot status
|
||||
func (s *Service) buildSnapshot(ctx context.Context, signedInUser *user.SignedInUser, maxItemsPerPartition uint32, metadata []byte, snapshotMeta cloudmigration.CloudMigrationSnapshot) error {
|
||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.buildSnapshot")
|
||||
|
@ -92,6 +92,7 @@ const (
|
||||
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
|
||||
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
|
||||
MuteTimingType MigrateDataType = "MUTE_TIMING"
|
||||
PluginDataType MigrateDataType = "PLUGIN"
|
||||
)
|
||||
|
||||
type ItemStatus string
|
||||
@ -116,7 +117,6 @@ const (
|
||||
ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT"
|
||||
ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE"
|
||||
ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR"
|
||||
ErrOnlyCoreDataSources ResourceErrorCode = "ONLY_CORE_DATA_SOURCES"
|
||||
ErrGeneric ResourceErrorCode = "GENERIC_ERROR"
|
||||
)
|
||||
|
||||
|
@ -5637,7 +5637,6 @@
|
||||
"RESOURCE_CONFLICT",
|
||||
"UNEXPECTED_STATUS_CODE",
|
||||
"INTERNAL_SERVICE_ERROR",
|
||||
"ONLY_CORE_DATA_SOURCES",
|
||||
"GENERIC_ERROR"
|
||||
]
|
||||
},
|
||||
@ -5674,7 +5673,8 @@
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING"
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -17137,7 +17137,6 @@
|
||||
"RESOURCE_CONFLICT",
|
||||
"UNEXPECTED_STATUS_CODE",
|
||||
"INTERNAL_SERVICE_ERROR",
|
||||
"ONLY_CORE_DATA_SOURCES",
|
||||
"GENERIC_ERROR"
|
||||
]
|
||||
},
|
||||
@ -17174,7 +17173,8 @@
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING"
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,18 @@ export default function MigrateToCloud() {
|
||||
to learn more about this feature!
|
||||
</Trans>
|
||||
</Alert>
|
||||
|
||||
<Alert
|
||||
title={t('migrate-to-cloud.public-preview.title-plugins', 'Migration of plugins')}
|
||||
buttonContent={''}
|
||||
severity={'info'}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.public-preview.message-plugins">
|
||||
Only Community and Commercial signed plugins are eligible for migration. Their latest version will be
|
||||
installed in the cloud instance, please upgrade your plugins before starting the migration process.
|
||||
</Trans>
|
||||
</Alert>
|
||||
|
||||
{config.cloudMigrationIsTarget ? <CloudPage /> : <OnPremPage />}
|
||||
</Page>
|
||||
);
|
||||
|
@ -182,7 +182,6 @@ export type MigrateDataResponseItemDto = {
|
||||
| 'RESOURCE_CONFLICT'
|
||||
| 'UNEXPECTED_STATUS_CODE'
|
||||
| 'INTERNAL_SERVICE_ERROR'
|
||||
| 'ONLY_CORE_DATA_SOURCES'
|
||||
| 'GENERIC_ERROR';
|
||||
message?: string;
|
||||
name?: string;
|
||||
@ -198,7 +197,8 @@ export type MigrateDataResponseItemDto = {
|
||||
| 'CONTACT_POINT'
|
||||
| 'NOTIFICATION_POLICY'
|
||||
| 'NOTIFICATION_TEMPLATE'
|
||||
| 'MUTE_TIMING';
|
||||
| 'MUTE_TIMING'
|
||||
| 'PLUGIN';
|
||||
};
|
||||
export type SnapshotResourceStats = {
|
||||
statuses?: {
|
||||
|
@ -1,55 +1,75 @@
|
||||
export * from './endpoints.gen';
|
||||
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
|
||||
|
||||
import { getLocalPlugins } from 'app/features/plugins/admin/api';
|
||||
import { LocalPlugin } from 'app/features/plugins/admin/types';
|
||||
|
||||
import { generatedAPI } from './endpoints.gen';
|
||||
|
||||
export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
|
||||
addTagTypes: ['cloud-migration-token', 'cloud-migration-session', 'cloud-migration-snapshot'],
|
||||
export const cloudMigrationAPI = generatedAPI
|
||||
.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
// Manually written because the Swagger specifications for the plugins endpoint do not exist
|
||||
getLocalPluginList: build.query<LocalPlugin[], void>({
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const list = await getLocalPlugins();
|
||||
return { data: list };
|
||||
} catch (error) {
|
||||
return { error: error };
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.enhanceEndpoints({
|
||||
addTagTypes: ['cloud-migration-token', 'cloud-migration-session', 'cloud-migration-snapshot'],
|
||||
|
||||
endpoints: {
|
||||
// Cloud-side - create token
|
||||
getCloudMigrationToken: {
|
||||
providesTags: ['cloud-migration-token'],
|
||||
},
|
||||
createCloudMigrationToken: {
|
||||
invalidatesTags: ['cloud-migration-token'],
|
||||
},
|
||||
deleteCloudMigrationToken: {
|
||||
invalidatesTags: ['cloud-migration-token'],
|
||||
},
|
||||
endpoints: {
|
||||
// Cloud-side - create token
|
||||
getCloudMigrationToken: {
|
||||
providesTags: ['cloud-migration-token'],
|
||||
},
|
||||
createCloudMigrationToken: {
|
||||
invalidatesTags: ['cloud-migration-token'],
|
||||
},
|
||||
deleteCloudMigrationToken: {
|
||||
invalidatesTags: ['cloud-migration-token'],
|
||||
},
|
||||
|
||||
// On-prem session management (entering token)
|
||||
getSessionList: {
|
||||
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
|
||||
},
|
||||
getSession: {
|
||||
providesTags: ['cloud-migration-session'],
|
||||
},
|
||||
createSession: {
|
||||
invalidatesTags: ['cloud-migration-session'],
|
||||
},
|
||||
deleteSession: {
|
||||
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
|
||||
},
|
||||
// On-prem session management (entering token)
|
||||
getSessionList: {
|
||||
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
|
||||
},
|
||||
getSession: {
|
||||
providesTags: ['cloud-migration-session'],
|
||||
},
|
||||
createSession: {
|
||||
invalidatesTags: ['cloud-migration-session'],
|
||||
},
|
||||
deleteSession: {
|
||||
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
|
||||
},
|
||||
|
||||
// Snapshot management
|
||||
getShapshotList: {
|
||||
providesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
getSnapshot: {
|
||||
providesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
createSnapshot: {
|
||||
invalidatesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
uploadSnapshot: {
|
||||
invalidatesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
// Snapshot management
|
||||
getShapshotList: {
|
||||
providesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
getSnapshot: {
|
||||
providesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
createSnapshot: {
|
||||
invalidatesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
uploadSnapshot: {
|
||||
invalidatesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
|
||||
getDashboardByUid: suppressErrorsOnQuery,
|
||||
getLibraryElementByUid: suppressErrorsOnQuery,
|
||||
},
|
||||
});
|
||||
getDashboardByUid: suppressErrorsOnQuery,
|
||||
getLibraryElementByUid: suppressErrorsOnQuery,
|
||||
getLocalPluginList: suppressErrorsOnQuery,
|
||||
},
|
||||
});
|
||||
|
||||
function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes extends string, ResultType>(
|
||||
endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType>
|
||||
@ -65,3 +85,5 @@ function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes
|
||||
return baseQuery;
|
||||
};
|
||||
}
|
||||
|
||||
export const { useGetLocalPluginListQuery } = cloudMigrationAPI;
|
||||
|
@ -9,6 +9,7 @@ import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
|
||||
import { LocalPlugin } from '../../plugins/admin/types';
|
||||
import { useGetDashboardByUidQuery, useGetLibraryElementByUidQuery } from '../api';
|
||||
|
||||
import { ResourceTableItem } from './types';
|
||||
@ -205,6 +206,7 @@ function BasicResourceInfo({ data }: { data: ResourceTableItem }) {
|
||||
function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
|
||||
const styles = useStyles2(getIconStyles);
|
||||
const datasource = useDatasource(resource.type === 'DATASOURCE' ? resource.refId : undefined);
|
||||
const pluginLogo = usePluginLogo(resource.type === 'PLUGIN' ? resource.plugin : undefined);
|
||||
|
||||
switch (resource.type) {
|
||||
case 'DASHBOARD':
|
||||
@ -229,6 +231,11 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
|
||||
return <Icon size="xl" name="bell" />;
|
||||
case 'ALERT_RULE':
|
||||
return <Icon size="xl" name="bell" />;
|
||||
case 'PLUGIN':
|
||||
if (pluginLogo) {
|
||||
return <img className={styles.icon} src={pluginLogo} alt="" />;
|
||||
}
|
||||
return <Icon size="xl" name="plug" />;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@ -257,3 +264,14 @@ function useDatasource(datasourceUID: string | undefined): DataSourceInstanceSet
|
||||
|
||||
return datasource;
|
||||
}
|
||||
|
||||
function usePluginLogo(plugin: LocalPlugin | undefined): string | undefined {
|
||||
const logos = useMemo(() => {
|
||||
if (!plugin) {
|
||||
return undefined;
|
||||
}
|
||||
return plugin?.info?.logos;
|
||||
}, [plugin]);
|
||||
|
||||
return logos?.small;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
useGetShapshotListQuery,
|
||||
useGetSnapshotQuery,
|
||||
useUploadSnapshotMutation,
|
||||
useGetLocalPluginListQuery,
|
||||
} from '../api';
|
||||
import { AlertWithTraceID } from '../shared/AlertWithTraceID';
|
||||
|
||||
@ -122,6 +123,8 @@ export const Page = () => {
|
||||
const [performCancelSnapshot, cancelSnapshotResult] = useCancelSnapshotMutation();
|
||||
const [performDisconnect, disconnectResult] = useDeleteSessionMutation();
|
||||
|
||||
const { currentData: localPlugins = [] } = useGetLocalPluginListQuery();
|
||||
|
||||
useNotifySuccessful(snapshot.data);
|
||||
|
||||
const sessionUid = session.data?.uid;
|
||||
@ -240,6 +243,7 @@ export const Page = () => {
|
||||
<Stack gap={4} direction="column">
|
||||
<ResourcesTable
|
||||
resources={snapshot.data.results}
|
||||
localPlugins={localPlugins}
|
||||
onChangePage={setPage}
|
||||
numberOfPages={Math.ceil((snapshot?.data?.stats?.total || 0) / PAGE_SIZE)}
|
||||
page={page}
|
||||
|
@ -53,11 +53,6 @@ function getTMessage(errorCode: MigrateDataResponseItemDto['errorCode']): string
|
||||
'migrate-to-cloud.resource-details.error-messages.resource-conflict',
|
||||
'There is a resource conflict with the target instance. Please check the Grafana server logs for more details.'
|
||||
);
|
||||
case 'ONLY_CORE_DATA_SOURCES':
|
||||
return t(
|
||||
'migrate-to-cloud.resource-details.error-messages.only-core-data-sources',
|
||||
'Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.'
|
||||
);
|
||||
case 'UNEXPECTED_STATUS_CODE':
|
||||
return t(
|
||||
'migrate-to-cloud.resource-details.error-messages.unexpected-error',
|
||||
|
@ -19,7 +19,13 @@ setBackendSrv(backendSrv);
|
||||
function render(props: Partial<ResourcesTableProps>) {
|
||||
rtlRender(
|
||||
<TestProvider>
|
||||
<ResourcesTable onChangePage={() => {}} numberOfPages={10} page={0} resources={props.resources || []} />
|
||||
<ResourcesTable
|
||||
onChangePage={() => {}}
|
||||
numberOfPages={10}
|
||||
page={0}
|
||||
resources={props.resources || []}
|
||||
localPlugins={[]}
|
||||
/>
|
||||
</TestProvider>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { InteractiveTable, Pagination, Stack } from '@grafana/ui';
|
||||
|
||||
import { LocalPlugin } from '../../plugins/admin/types';
|
||||
import { MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
import { NameCell } from './NameCell';
|
||||
@ -12,6 +13,7 @@ import { ResourceTableItem } from './types';
|
||||
|
||||
export interface ResourcesTableProps {
|
||||
resources: MigrateDataResponseItemDto[];
|
||||
localPlugins: LocalPlugin[];
|
||||
page: number;
|
||||
numberOfPages: number;
|
||||
onChangePage: (page: number) => void;
|
||||
@ -23,7 +25,13 @@ const columns = [
|
||||
{ id: 'status', header: 'Status', cell: StatusCell },
|
||||
];
|
||||
|
||||
export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, page = 1 }: ResourcesTableProps) {
|
||||
export function ResourcesTable({
|
||||
resources,
|
||||
localPlugins,
|
||||
numberOfPages = 0,
|
||||
onChangePage,
|
||||
page = 1,
|
||||
}: ResourcesTableProps) {
|
||||
const [focusedResource, setfocusedResource] = useState<ResourceTableItem | undefined>();
|
||||
|
||||
const handleShowDetailsModal = useCallback((resource: ResourceTableItem) => {
|
||||
@ -31,8 +39,16 @@ export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, pag
|
||||
}, []);
|
||||
|
||||
const data = useMemo(() => {
|
||||
return resources.map((r) => ({ ...r, showDetails: handleShowDetailsModal }));
|
||||
}, [resources, handleShowDetailsModal]);
|
||||
return resources.map((r) => {
|
||||
const plugin = getPlugin(r, localPlugins);
|
||||
|
||||
return {
|
||||
...r,
|
||||
showDetails: handleShowDetailsModal,
|
||||
plugin: plugin,
|
||||
};
|
||||
});
|
||||
}, [resources, handleShowDetailsModal, localPlugins]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -46,3 +62,14 @@ export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, pag
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getPlugin(
|
||||
r: MigrateDataResponseItemDto | undefined,
|
||||
plugins: LocalPlugin[] | undefined
|
||||
): LocalPlugin | undefined {
|
||||
if (!r || !plugins || r.type !== 'PLUGIN') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return plugins.find((plugin) => plugin.id === r.refId);
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) {
|
||||
return t('migrate-to-cloud.resource-type.notification_policy', 'Notification Policy');
|
||||
case 'ALERT_RULE':
|
||||
return t('migrate-to-cloud.resource-type.alert_rule', 'Alert Rule');
|
||||
case 'PLUGIN':
|
||||
return t('migrate-to-cloud.resource-type.plugin', 'Plugin');
|
||||
default:
|
||||
return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { LocalPlugin } from '../../plugins/admin/types';
|
||||
import { MigrateDataResponseItemDto } from '../api';
|
||||
|
||||
export interface ResourceTableItem extends MigrateDataResponseItemDto {
|
||||
showDetails: (resource: ResourceTableItem) => void;
|
||||
plugin: LocalPlugin | undefined;
|
||||
}
|
||||
|
@ -62,6 +62,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) {
|
||||
types.push(t('migrate-to-cloud.migrated-counts.notification_policies', 'notification policies'));
|
||||
} else if (type === 'ALERT_RULE') {
|
||||
types.push(t('migrate-to-cloud.migrated-counts.alert_rules', 'alert rules'));
|
||||
} else if (type === 'PLUGIN') {
|
||||
types.push(t('migrate-to-cloud.migrated-counts.plugins', 'plugins'));
|
||||
}
|
||||
|
||||
distinctItems += 1;
|
||||
|
@ -1873,7 +1873,8 @@
|
||||
"library_elements": "library elements",
|
||||
"mute_timings": "mute timings",
|
||||
"notification_policies": "notification policies",
|
||||
"notification_templates": "notification templates"
|
||||
"notification_templates": "notification templates",
|
||||
"plugins": "plugins"
|
||||
},
|
||||
"migration-token": {
|
||||
"delete-button": "Delete token",
|
||||
@ -1924,7 +1925,9 @@
|
||||
"public-preview": {
|
||||
"button-text": "Give feedback",
|
||||
"message": "No SLAs are available yet. <2>Visit our docs</2> to learn more about this feature!",
|
||||
"title": "Migrate to Grafana Cloud is in public preview"
|
||||
"message-plugins": "Only Community and Commercial signed plugins are eligible for migration. Their latest version will be installed in the cloud instance, please upgrade your plugins before starting the migration process.",
|
||||
"title": "Migrate to Grafana Cloud is in public preview",
|
||||
"title-plugins": "Migration of plugins"
|
||||
},
|
||||
"resource-details": {
|
||||
"dismiss-button": "OK",
|
||||
@ -1937,7 +1940,6 @@
|
||||
"generic-error": "There has been an error while migrating. Please check the cloud migration logs for more information.",
|
||||
"internal-service-error": "There has been an error while migrating. Please check the Grafana server logs for more details.",
|
||||
"library-element-name-conflict": "There is a library element with the same name in the target instance. Rename one of them and try again.",
|
||||
"only-core-data-sources": "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.",
|
||||
"resource-conflict": "There is a resource conflict with the target instance. Please check the Grafana server logs for more details.",
|
||||
"unexpected-error": "There has been an error while migrating. Please check the Grafana server logs for more details.",
|
||||
"unsupported-data-type": "Migration of this data type is not currently supported."
|
||||
@ -1976,6 +1978,7 @@
|
||||
"mute_timing": "Mute Timing",
|
||||
"notification_policy": "Notification Policy",
|
||||
"notification_template": "Notification Template",
|
||||
"plugin": "Plugin",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"summary": {
|
||||
|
@ -1873,7 +1873,8 @@
|
||||
"library_elements": "ľįþřäřy ęľęmęʼnŧş",
|
||||
"mute_timings": "mūŧę ŧįmįʼnģş",
|
||||
"notification_policies": "ʼnőŧįƒįčäŧįőʼn pőľįčįęş",
|
||||
"notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş"
|
||||
"notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş",
|
||||
"plugins": "pľūģįʼnş"
|
||||
},
|
||||
"migration-token": {
|
||||
"delete-button": "Đęľęŧę ŧőĸęʼn",
|
||||
@ -1924,7 +1925,9 @@
|
||||
"public-preview": {
|
||||
"button-text": "Ğįvę ƒęęđþäčĸ",
|
||||
"message": "Ńő ŜĿÅş äřę äväįľäþľę yęŧ. <2>Vįşįŧ őūř đőčş</2> ŧő ľęäřʼn mőřę äþőūŧ ŧĥįş ƒęäŧūřę!",
|
||||
"title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ"
|
||||
"message-plugins": "Øʼnľy Cőmmūʼnįŧy äʼnđ Cőmmęřčįäľ şįģʼnęđ pľūģįʼnş äřę ęľįģįþľę ƒőř mįģřäŧįőʼn. Ŧĥęįř ľäŧęşŧ vęřşįőʼn ŵįľľ þę įʼnşŧäľľęđ įʼn ŧĥę čľőūđ įʼnşŧäʼnčę, pľęäşę ūpģřäđę yőūř pľūģįʼnş þęƒőřę şŧäřŧįʼnģ ŧĥę mįģřäŧįőʼn přőčęşş.",
|
||||
"title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ",
|
||||
"title-plugins": "Mįģřäŧįőʼn őƒ pľūģįʼnş"
|
||||
},
|
||||
"resource-details": {
|
||||
"dismiss-button": "ØĶ",
|
||||
@ -1937,7 +1940,6 @@
|
||||
"generic-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę čľőūđ mįģřäŧįőʼn ľőģş ƒőř mőřę įʼnƒőřmäŧįőʼn.",
|
||||
"internal-service-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||
"library-element-name-conflict": "Ŧĥęřę įş ä ľįþřäřy ęľęmęʼnŧ ŵįŧĥ ŧĥę şämę ʼnämę įʼn ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Ŗęʼnämę őʼnę őƒ ŧĥęm äʼnđ ŧřy äģäįʼn.",
|
||||
"only-core-data-sources": "Øʼnľy čőřę đäŧä şőūřčęş äřę şūppőřŧęđ. Pľęäşę ęʼnşūřę ŧĥę pľūģįʼn įş įʼnşŧäľľęđ őʼn ŧĥę čľőūđ şŧäčĸ.",
|
||||
"resource-conflict": "Ŧĥęřę įş ä řęşőūřčę čőʼnƒľįčŧ ŵįŧĥ ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||
"unexpected-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||
"unsupported-data-type": "Mįģřäŧįőʼn őƒ ŧĥįş đäŧä ŧypę įş ʼnőŧ čūřřęʼnŧľy şūppőřŧęđ."
|
||||
@ -1976,6 +1978,7 @@
|
||||
"mute_timing": "Mūŧę Ŧįmįʼnģ",
|
||||
"notification_policy": "Ńőŧįƒįčäŧįőʼn Pőľįčy",
|
||||
"notification_template": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧę",
|
||||
"plugin": "Pľūģįʼn",
|
||||
"unknown": "Ůʼnĸʼnőŵʼn"
|
||||
},
|
||||
"summary": {
|
||||
|
@ -7205,7 +7205,6 @@
|
||||
"RESOURCE_CONFLICT",
|
||||
"UNEXPECTED_STATUS_CODE",
|
||||
"INTERNAL_SERVICE_ERROR",
|
||||
"ONLY_CORE_DATA_SOURCES",
|
||||
"GENERIC_ERROR"
|
||||
],
|
||||
"type": "string"
|
||||
@ -7242,7 +7241,8 @@
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING"
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user