CloudMigration: Show warning message for successfully migrated non-core data sources (#91545)
* minor performance improvement * apply a warning to any non-core plugins that successfully migrate * commit frontend wip while I refactor some stuff * update api * repurpose error dialog to be a generic details dialog * whitespace * add unit test * fixes from testing * fix migration summary * add comment * fix localization stuff * fix backend test * reduce number of queries to the db * some PR feedback * whitespace
This commit is contained in:
@@ -435,10 +435,10 @@ func (cma *CloudMigrationAPI) GetSnapshot(c *contextmodel.ReqContext) response.R
|
|||||||
dtoResults := make([]MigrateDataResponseItemDTO, len(results))
|
dtoResults := make([]MigrateDataResponseItemDTO, len(results))
|
||||||
for i := 0; i < len(results); i++ {
|
for i := 0; i < len(results); i++ {
|
||||||
dtoResults[i] = MigrateDataResponseItemDTO{
|
dtoResults[i] = MigrateDataResponseItemDTO{
|
||||||
Type: MigrateDataType(results[i].Type),
|
Type: MigrateDataType(results[i].Type),
|
||||||
RefID: results[i].RefID,
|
RefID: results[i].RefID,
|
||||||
Status: ItemStatus(results[i].Status),
|
Status: ItemStatus(results[i].Status),
|
||||||
Error: results[i].Error,
|
Message: results[i].Error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ func TestCloudMigrationAPI_RunMigration(t *testing.T) {
|
|||||||
requestUrl: "/api/cloudmigration/migration/1234/run",
|
requestUrl: "/api/cloudmigration/migration/1234/run",
|
||||||
basicRole: org.RoleAdmin,
|
basicRole: org.RoleAdmin,
|
||||||
expectedHttpResult: http.StatusOK,
|
expectedHttpResult: http.StatusOK,
|
||||||
expectedBody: `{"uid":"fake_uid","items":[{"type":"type","refId":"make_refid","status":"ok","error":"none"}]}`,
|
expectedBody: `{"uid":"fake_uid","items":[{"type":"type","refId":"make_refid","status":"ok","message":"none"}]}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should return 403 if no used is not admin",
|
desc: "should return 403 if no used is not admin",
|
||||||
@@ -303,7 +303,7 @@ func TestCloudMigrationAPI_GetMigrationRun(t *testing.T) {
|
|||||||
requestUrl: "/api/cloudmigration/migration/run/1234",
|
requestUrl: "/api/cloudmigration/migration/run/1234",
|
||||||
basicRole: org.RoleAdmin,
|
basicRole: org.RoleAdmin,
|
||||||
expectedHttpResult: http.StatusOK,
|
expectedHttpResult: http.StatusOK,
|
||||||
expectedBody: `{"uid":"fake_uid","items":[{"type":"type","refId":"make_refid","status":"ok","error":"none"}]}`,
|
expectedBody: `{"uid":"fake_uid","items":[{"type":"type","refId":"make_refid","status":"ok","message":"none"}]}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should return 403 if no used is not admin",
|
desc: "should return 403 if no used is not admin",
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ type MigrateDataResponseItemDTO struct {
|
|||||||
// required:true
|
// required:true
|
||||||
RefID string `json:"refId"`
|
RefID string `json:"refId"`
|
||||||
// required:true
|
// required:true
|
||||||
Status ItemStatus `json:"status"`
|
Status ItemStatus `json:"status"`
|
||||||
Error string `json:"error,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:enum MigrateDataType
|
// swagger:enum MigrateDataType
|
||||||
@@ -129,6 +129,7 @@ type ItemStatus string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ItemStatusOK ItemStatus = "OK"
|
ItemStatusOK ItemStatus = "OK"
|
||||||
|
ItemStatusWarning ItemStatus = "WARNING"
|
||||||
ItemStatusError ItemStatus = "ERROR"
|
ItemStatusError ItemStatus = "ERROR"
|
||||||
ItemStatusPending ItemStatus = "PENDING"
|
ItemStatusPending ItemStatus = "PENDING"
|
||||||
ItemStatusUnknown ItemStatus = "UNKNOWN"
|
ItemStatusUnknown ItemStatus = "UNKNOWN"
|
||||||
@@ -192,10 +193,10 @@ func convertMigrateDataResponseToDTO(r cloudmigration.MigrateDataResponse) Migra
|
|||||||
for i := 0; i < len(r.Items); i++ {
|
for i := 0; i < len(r.Items); i++ {
|
||||||
item := r.Items[i]
|
item := r.Items[i]
|
||||||
items[i] = MigrateDataResponseItemDTO{
|
items[i] = MigrateDataResponseItemDTO{
|
||||||
Type: MigrateDataType(item.Type),
|
Type: MigrateDataType(item.Type),
|
||||||
RefID: item.RefID,
|
RefID: item.RefID,
|
||||||
Status: ItemStatus(item.Status),
|
Status: ItemStatus(item.Status),
|
||||||
Error: item.Error,
|
Message: item.Error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MigrateDataResponseDTO{
|
return MigrateDataResponseDTO{
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/gcom"
|
"github.com/grafana/grafana/pkg/services/gcom"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@@ -57,6 +58,7 @@ type Service struct {
|
|||||||
gcomService gcom.Service
|
gcomService gcom.Service
|
||||||
dashboardService dashboards.DashboardService
|
dashboardService dashboards.DashboardService
|
||||||
folderService folder.Service
|
folderService folder.Service
|
||||||
|
pluginStore pluginstore.Store
|
||||||
secretsService secrets.Service
|
secretsService secrets.Service
|
||||||
kvStore *kvstore.NamespacedKVStore
|
kvStore *kvstore.NamespacedKVStore
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ func ProvideService(
|
|||||||
tracer tracing.Tracer,
|
tracer tracing.Tracer,
|
||||||
dashboardService dashboards.DashboardService,
|
dashboardService dashboards.DashboardService,
|
||||||
folderService folder.Service,
|
folderService folder.Service,
|
||||||
|
pluginStore pluginstore.Store,
|
||||||
kvStore kvstore.KVStore,
|
kvStore kvstore.KVStore,
|
||||||
) (cloudmigration.Service, error) {
|
) (cloudmigration.Service, error) {
|
||||||
if !features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) {
|
if !features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) {
|
||||||
@@ -107,6 +110,7 @@ func ProvideService(
|
|||||||
secretsService: secretsService,
|
secretsService: secretsService,
|
||||||
dashboardService: dashboardService,
|
dashboardService: dashboardService,
|
||||||
folderService: folderService,
|
folderService: folderService,
|
||||||
|
pluginStore: pluginStore,
|
||||||
kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"),
|
kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"),
|
||||||
}
|
}
|
||||||
s.api = api.RegisterApi(routeRegister, s, tracer)
|
s.api = api.RegisterApi(routeRegister, s, tracer)
|
||||||
@@ -590,12 +594,19 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps
|
|||||||
return snapshot, nil
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// We need to update the snapshot in our db before reporting anything
|
// We need to update the snapshot in our db before reporting anything
|
||||||
if err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{
|
if err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{
|
||||||
UID: snapshot.UID,
|
UID: snapshot.UID,
|
||||||
SessionID: sessionUid,
|
SessionID: sessionUid,
|
||||||
Status: localStatus,
|
Status: localStatus,
|
||||||
Resources: snapshotMeta.Results,
|
Resources: resources,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, fmt.Errorf("error updating snapshot status: %w", err)
|
return nil, fmt.Errorf("error updating snapshot status: %w", err)
|
||||||
}
|
}
|
||||||
@@ -777,3 +788,39 @@ func (s *Service) getLocalEventId(ctx context.Context) (string, error) {
|
|||||||
|
|
||||||
return anonId, nil
|
return anonId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.Error = "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack."
|
||||||
|
}
|
||||||
|
|
||||||
|
results[i] = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||||
"github.com/grafana/grafana/pkg/services/cloudmigration/gmsclient"
|
"github.com/grafana/grafana/pkg/services/cloudmigration/gmsclient"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||||
@@ -23,6 +24,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||||
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@@ -399,6 +401,122 @@ func Test_SortFolders(t *testing.T) {
|
|||||||
require.Equal(t, expected, sortedFolders)
|
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
|
||||||
|
sess, err := s.store.CreateMigrationSession(context.Background(), cloudmigration.CloudMigrationSession{})
|
||||||
|
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{
|
||||||
|
{
|
||||||
|
Type: cloudmigration.DatasourceDataType,
|
||||||
|
RefID: "1", // this will be core
|
||||||
|
Status: cloudmigration.ItemStatusOK,
|
||||||
|
SnapshotUID: snapshotUid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: cloudmigration.DatasourceDataType,
|
||||||
|
RefID: "2", // this will be non-core
|
||||||
|
Status: cloudmigration.ItemStatusOK,
|
||||||
|
SnapshotUID: snapshotUid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: cloudmigration.DatasourceDataType,
|
||||||
|
RefID: "3", // this will be non-core with an error
|
||||||
|
Status: cloudmigration.ItemStatusError,
|
||||||
|
Error: "please don't overwrite me",
|
||||||
|
SnapshotUID: snapshotUid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the snapshot with results
|
||||||
|
snapshot, err := s.GetSnapshot(ctxWithSignedInUser(), cloudmigration.GetSnapshotsQuery{
|
||||||
|
SnapshotUID: snapshotUid,
|
||||||
|
SessionUID: sess.UID,
|
||||||
|
ResultPage: 1,
|
||||||
|
ResultLimit: 10,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, snapshot.Resources, 4)
|
||||||
|
|
||||||
|
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 ctxWithSignedInUser() context.Context {
|
func ctxWithSignedInUser() context.Context {
|
||||||
c := &contextmodel.ReqContext{
|
c := &contextmodel.ReqContext{
|
||||||
SignedInUser: &user.SignedInUser{OrgID: 1},
|
SignedInUser: &user.SignedInUser{OrgID: 1},
|
||||||
@@ -461,6 +579,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi
|
|||||||
tracer,
|
tracer,
|
||||||
dashboardService,
|
dashboardService,
|
||||||
mockFolder,
|
mockFolder,
|
||||||
|
&pluginstore.FakePluginStore{},
|
||||||
kvstore.ProvideService(sqlStore),
|
kvstore.ProvideService(sqlStore),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -294,11 +294,6 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li
|
|||||||
// CreateUpdateSnapshotResources either updates a migration resource for a snapshot, or creates it if it does not exist
|
// CreateUpdateSnapshotResources either updates a migration resource for a snapshot, or creates it if it does not exist
|
||||||
// If the uid is not known, it uses snapshot_uid + resource_uid as a lookup
|
// If the uid is not known, it uses snapshot_uid + resource_uid as a lookup
|
||||||
func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error {
|
func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error {
|
||||||
// ensure snapshot_uids are consistent so that we can use them to query when uid isn't known
|
|
||||||
for i := 0; i < len(resources); i++ {
|
|
||||||
resources[i].SnapshotUID = snapshotUid
|
|
||||||
}
|
|
||||||
|
|
||||||
return ss.db.InTransaction(ctx, func(ctx context.Context) error {
|
return ss.db.InTransaction(ctx, func(ctx context.Context) error {
|
||||||
sql := "UPDATE cloud_migration_resource SET status=?, error_string=? WHERE uid=? OR (snapshot_uid=? AND resource_uid=?)"
|
sql := "UPDATE cloud_migration_resource SET status=?, error_string=? WHERE uid=? OR (snapshot_uid=? AND resource_uid=?)"
|
||||||
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
@@ -314,6 +309,8 @@ func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotU
|
|||||||
return err
|
return err
|
||||||
} else if n == 0 {
|
} else if n == 0 {
|
||||||
r.UID = util.GenerateShortUID()
|
r.UID = util.GenerateShortUID()
|
||||||
|
// ensure snapshot_uids are consistent so that we can use them to query when uid isn't known
|
||||||
|
r.SnapshotUID = snapshotUid
|
||||||
_, err := sess.Insert(r)
|
_, err := sess.Insert(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -105,6 +105,12 @@ func (c *memoryClientImpl) GetSnapshotStatus(ctx context.Context, session cloudm
|
|||||||
RefID: "folder1",
|
RefID: "folder1",
|
||||||
Status: cloudmigration.ItemStatusOK,
|
Status: cloudmigration.ItemStatusOK,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: cloudmigration.DatasourceDataType,
|
||||||
|
RefID: "ds2",
|
||||||
|
Status: cloudmigration.ItemStatusWarning,
|
||||||
|
Error: "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ type ItemStatus string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ItemStatusOK ItemStatus = "OK"
|
ItemStatusOK ItemStatus = "OK"
|
||||||
|
ItemStatusWarning ItemStatus = "WARNING"
|
||||||
ItemStatusError ItemStatus = "ERROR"
|
ItemStatusError ItemStatus = "ERROR"
|
||||||
ItemStatusPending ItemStatus = "PENDING"
|
ItemStatusPending ItemStatus = "PENDING"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16974,7 +16974,7 @@
|
|||||||
"status"
|
"status"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"error": {
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"refId": {
|
"refId": {
|
||||||
@@ -16984,6 +16984,7 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"OK",
|
"OK",
|
||||||
|
"WARNING",
|
||||||
"ERROR",
|
"ERROR",
|
||||||
"PENDING",
|
"PENDING",
|
||||||
"UNKNOWN"
|
"UNKNOWN"
|
||||||
|
|||||||
@@ -154,9 +154,9 @@ export type CreateSnapshotResponseDto = {
|
|||||||
uid?: string;
|
uid?: string;
|
||||||
};
|
};
|
||||||
export type MigrateDataResponseItemDto = {
|
export type MigrateDataResponseItemDto = {
|
||||||
error?: string;
|
message?: string;
|
||||||
refId: string;
|
refId: string;
|
||||||
status: 'OK' | 'ERROR' | 'PENDING' | 'UNKNOWN';
|
status: 'OK' | 'WARNING' | 'ERROR' | 'PENDING' | 'UNKNOWN';
|
||||||
type: 'DASHBOARD' | 'DATASOURCE' | 'FOLDER';
|
type: 'DASHBOARD' | 'DATASOURCE' | 'FOLDER';
|
||||||
};
|
};
|
||||||
export type SnapshotResourceStats = {
|
export type SnapshotResourceStats = {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export function MigrationSummary(props: MigrationSummaryProps) {
|
|||||||
const totalCount = snapshot?.stats?.total ?? 0;
|
const totalCount = snapshot?.stats?.total ?? 0;
|
||||||
const errorCount = snapshot?.stats?.statuses?.['ERROR'] ?? 0;
|
const errorCount = snapshot?.stats?.statuses?.['ERROR'] ?? 0;
|
||||||
const successCount = snapshot?.stats?.statuses?.['OK'] ?? 0;
|
const successCount = snapshot?.stats?.statuses?.['OK'] ?? 0;
|
||||||
|
const warningCount = snapshot?.stats?.statuses?.['WARNING'] ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -83,7 +84,7 @@ export function MigrationSummary(props: MigrationSummaryProps) {
|
|||||||
</MigrationInfo>
|
</MigrationInfo>
|
||||||
|
|
||||||
<MigrationInfo title={t('migrate-to-cloud.summary.successful-resource-count', 'Successfully migrated')}>
|
<MigrationInfo title={t('migrate-to-cloud.summary.successful-resource-count', 'Successfully migrated')}>
|
||||||
{successCount}
|
{successCount + warningCount}
|
||||||
</MigrationInfo>
|
</MigrationInfo>
|
||||||
|
|
||||||
<MigrationInfo title={t('migrate-to-cloud.summary.target-stack-title', 'Uploading to')}>
|
<MigrationInfo title={t('migrate-to-cloud.summary.target-stack-title', 'Uploading to')}>
|
||||||
|
|||||||
@@ -351,14 +351,27 @@ function getError(props: GetErrorProps): ErrorDescription | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const errorCount = snapshot?.stats?.statuses?.['ERROR'] ?? 0;
|
const errorCount = snapshot?.stats?.statuses?.['ERROR'] ?? 0;
|
||||||
if (snapshot?.status === 'FINISHED' && errorCount > 0) {
|
const warningCount = snapshot?.stats?.statuses?.['WARNING'] ?? 0;
|
||||||
|
if (snapshot?.status === 'FINISHED' && errorCount + warningCount > 0) {
|
||||||
|
let msgBody = '';
|
||||||
|
|
||||||
|
// If there are any errors, that's the most pressing info. If there are no errors but warnings, show the warning text instead.
|
||||||
|
if (errorCount > 0) {
|
||||||
|
msgBody = t(
|
||||||
|
'migrate-to-cloud.onprem.migration-finished-with-errors-body',
|
||||||
|
'The migration has completed, but some items could not be migrated to the cloud stack. Check the failed resources for more details'
|
||||||
|
);
|
||||||
|
} else if (warningCount > 0) {
|
||||||
|
msgBody = t(
|
||||||
|
'migrate-to-cloud.onprem.migration-finished-with-warnings-body',
|
||||||
|
'The migration has completed with some warnings. Check individual resources for more details'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
title: t('migrate-to-cloud.onprem.some-resources-errored-title', 'Resource migration complete'),
|
title: t('migrate-to-cloud.onprem.migration-finished-with-caveat-title', 'Resource migration complete'),
|
||||||
body: t(
|
body: msgBody,
|
||||||
'migrate-to-cloud.onprem.some-resources-errored-body',
|
|
||||||
'The migration has completed, but some items could not be migrated to the cloud stack. Check the failed resources for more details'
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-11
@@ -4,49 +4,54 @@ import { Trans, t } from 'app/core/internationalization';
|
|||||||
import { prettyTypeName } from './TypeCell';
|
import { prettyTypeName } from './TypeCell';
|
||||||
import { ResourceTableItem } from './types';
|
import { ResourceTableItem } from './types';
|
||||||
|
|
||||||
interface ResourceErrorModalProps {
|
interface ResourceDetailsModalProps {
|
||||||
resource: ResourceTableItem | undefined;
|
resource: ResourceTableItem | undefined;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResourceErrorModal(props: ResourceErrorModalProps) {
|
export function ResourceDetailsModal(props: ResourceDetailsModalProps) {
|
||||||
const { resource, onClose } = props;
|
const { resource, onClose } = props;
|
||||||
|
|
||||||
const refId = resource?.refId;
|
const refId = resource?.refId;
|
||||||
const typeName = resource && prettyTypeName(resource.type);
|
const typeName = resource && prettyTypeName(resource.type);
|
||||||
|
|
||||||
|
let msgTitle = t('migrate-to-cloud.resource-details.generic-title', 'Resource migration details:');
|
||||||
|
if (resource?.status === 'ERROR') {
|
||||||
|
msgTitle = t('migrate-to-cloud.resource-details.error-title', 'Unable to migrate this resource:');
|
||||||
|
} else if (resource?.status === 'WARNING') {
|
||||||
|
msgTitle = t('migrate-to-cloud.resource-details.warning-title', 'Resource migrated with a warning:');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t('migrate-to-cloud.resource-error.title', 'Unable to migrate this resource')}
|
title={t('migrate-to-cloud.resource-details.title', 'Migration resource details')}
|
||||||
isOpen={Boolean(resource)}
|
isOpen={Boolean(resource)}
|
||||||
onDismiss={onClose}
|
onDismiss={onClose}
|
||||||
>
|
>
|
||||||
{resource && (
|
{resource && (
|
||||||
<Stack direction="column" gap={2} alignItems="flex-start">
|
<Stack direction="column" gap={2} alignItems="flex-start">
|
||||||
<Text element="p" weight="bold">
|
<Text element="p" weight="bold">
|
||||||
<Trans i18nKey="migrate-to-cloud.resource-error.resource-summary">
|
<Trans i18nKey="migrate-to-cloud.resource-details.resource-summary">
|
||||||
{{ refId }} ({{ typeName }})
|
{{ refId }} ({{ typeName }})
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{resource.error ? (
|
{resource.message ? (
|
||||||
<>
|
<>
|
||||||
<Text element="p">
|
<Text element="p">{msgTitle}</Text>
|
||||||
<Trans i18nKey="migrate-to-cloud.resource-error.specific-error">The specific error was:</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text element="p" weight="bold">
|
<Text element="p" weight="bold">
|
||||||
{resource.error}
|
{resource.message}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text element="p">
|
<Text element="p">
|
||||||
<Trans i18nKey="migrate-to-cloud.resource-error.unknown-error">An unknown error occurred.</Trans>
|
<Trans i18nKey="migrate-to-cloud.resource-details.missing-message">No message provided.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button onClick={onClose}>
|
<Button onClick={onClose}>
|
||||||
<Trans i18nKey="migrate-to-cloud.resource-error.dismiss-button">OK</Trans>
|
<Trans i18nKey="migrate-to-cloud.resource-details.dismiss-button">OK</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -99,7 +99,7 @@ describe('ResourcesTable', () => {
|
|||||||
expect(screen.getByText('Uploaded to cloud')).toBeInTheDocument();
|
expect(screen.getByText('Uploaded to cloud')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the success error correctly', () => {
|
it('renders the error status correctly', () => {
|
||||||
const resources = [
|
const resources = [
|
||||||
wellFormedDatasourceMigrationItem(1, {
|
wellFormedDatasourceMigrationItem(1, {
|
||||||
refId: datasourceA.uid,
|
refId: datasourceA.uid,
|
||||||
@@ -112,12 +112,43 @@ describe('ResourcesTable', () => {
|
|||||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a details button when there's an error description", () => {
|
it("shows a details button when there's an error message", () => {
|
||||||
const resources = [
|
const resources = [
|
||||||
wellFormedDatasourceMigrationItem(1, {
|
wellFormedDatasourceMigrationItem(1, {
|
||||||
refId: datasourceA.uid,
|
refId: datasourceA.uid,
|
||||||
status: 'ERROR',
|
status: 'ERROR',
|
||||||
error: 'Some error',
|
message: 'Some error',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render({ resources });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', {
|
||||||
|
name: 'Details',
|
||||||
|
})
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the warning status correctly', () => {
|
||||||
|
const resources = [
|
||||||
|
wellFormedDatasourceMigrationItem(1, {
|
||||||
|
refId: datasourceA.uid,
|
||||||
|
status: 'WARNING',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
render({ resources });
|
||||||
|
|
||||||
|
expect(screen.getByText('Uploaded with warning')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a details button when there's a warning message", () => {
|
||||||
|
const resources = [
|
||||||
|
wellFormedDatasourceMigrationItem(1, {
|
||||||
|
refId: datasourceA.uid,
|
||||||
|
status: 'WARNING',
|
||||||
|
message: 'Some warning',
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { InteractiveTable, Pagination, Stack } from '@grafana/ui';
|
|||||||
import { MigrateDataResponseItemDto } from '../api';
|
import { MigrateDataResponseItemDto } from '../api';
|
||||||
|
|
||||||
import { NameCell } from './NameCell';
|
import { NameCell } from './NameCell';
|
||||||
import { ResourceErrorModal } from './ResourceErrorModal';
|
import { ResourceDetailsModal } from './ResourceDetailsModal';
|
||||||
import { StatusCell } from './StatusCell';
|
import { StatusCell } from './StatusCell';
|
||||||
import { TypeCell } from './TypeCell';
|
import { TypeCell } from './TypeCell';
|
||||||
import { ResourceTableItem } from './types';
|
import { ResourceTableItem } from './types';
|
||||||
@@ -24,15 +24,15 @@ const columns = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, page = 1 }: ResourcesTableProps) {
|
export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, page = 1 }: ResourcesTableProps) {
|
||||||
const [erroredResource, setErroredResource] = useState<ResourceTableItem | undefined>();
|
const [focusedResource, setfocusedResource] = useState<ResourceTableItem | undefined>();
|
||||||
|
|
||||||
const handleShowErrorModal = useCallback((resource: ResourceTableItem) => {
|
const handleShowDetailsModal = useCallback((resource: ResourceTableItem) => {
|
||||||
setErroredResource(resource);
|
setfocusedResource(resource);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
return resources.map((r) => ({ ...r, showError: handleShowErrorModal }));
|
return resources.map((r) => ({ ...r, showDetails: handleShowDetailsModal }));
|
||||||
}, [resources, handleShowErrorModal]);
|
}, [resources, handleShowDetailsModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -42,7 +42,7 @@ export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, pag
|
|||||||
<Pagination numberOfPages={numberOfPages} currentPage={page} onNavigate={onChangePage} />
|
<Pagination numberOfPages={numberOfPages} currentPage={page} onNavigate={onChangePage} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<ResourceErrorModal resource={erroredResource} onClose={() => setErroredResource(undefined)} />
|
<ResourceDetailsModal resource={focusedResource} onClose={() => setfocusedResource(undefined)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export function StatusCell(props: CellProps<ResourceTableItem>) {
|
|||||||
return <Text color="secondary">{t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')}</Text>;
|
return <Text color="secondary">{t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')}</Text>;
|
||||||
} else if (item.status === 'OK') {
|
} else if (item.status === 'OK') {
|
||||||
return <Text color="success">{t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')}</Text>;
|
return <Text color="success">{t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')}</Text>;
|
||||||
|
} else if (item.status === 'WARNING') {
|
||||||
|
return <WarningCell item={item} />;
|
||||||
} else if (item.status === 'ERROR') {
|
} else if (item.status === 'ERROR') {
|
||||||
return <ErrorCell item={item} />;
|
return <ErrorCell item={item} />;
|
||||||
}
|
}
|
||||||
@@ -25,11 +27,25 @@ function ErrorCell({ item }: { item: ResourceTableItem }) {
|
|||||||
<Stack alignItems="center">
|
<Stack alignItems="center">
|
||||||
<Text color="error">{t('migrate-to-cloud.resource-status.failed', 'Error')}</Text>
|
<Text color="error">{t('migrate-to-cloud.resource-status.failed', 'Error')}</Text>
|
||||||
|
|
||||||
{item.error && (
|
{item.message && (
|
||||||
<Button size="sm" variant="secondary" onClick={() => item.showError(item)}>
|
<Button size="sm" variant="secondary" onClick={() => item.showDetails(item)}>
|
||||||
{t('migrate-to-cloud.resource-status.error-details-button', 'Details')}
|
{t('migrate-to-cloud.resource-status.error-details-button', 'Details')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WarningCell({ item }: { item: ResourceTableItem }) {
|
||||||
|
return (
|
||||||
|
<Stack alignItems="center">
|
||||||
|
<Text color="warning">{t('migrate-to-cloud.resource-status.warning', 'Uploaded with warning')}</Text>
|
||||||
|
|
||||||
|
{item.message && (
|
||||||
|
<Button size="sm" variant="secondary" onClick={() => item.showDetails(item)}>
|
||||||
|
{t('migrate-to-cloud.resource-status.warning-details-button', 'Details')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MigrateDataResponseItemDto } from '../api';
|
import { MigrateDataResponseItemDto } from '../api';
|
||||||
|
|
||||||
export interface ResourceTableItem extends MigrateDataResponseItemDto {
|
export interface ResourceTableItem extends MigrateDataResponseItemDto {
|
||||||
showError: (resource: ResourceTableItem) => void;
|
showDetails: (resource: ResourceTableItem) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1115,10 +1115,11 @@
|
|||||||
"error-see-server-logs": "See the Grafana server logs for more details",
|
"error-see-server-logs": "See the Grafana server logs for more details",
|
||||||
"get-session-error-title": "Error loading migration configuration",
|
"get-session-error-title": "Error loading migration configuration",
|
||||||
"get-snapshot-error-title": "Error loading snapshot",
|
"get-snapshot-error-title": "Error loading snapshot",
|
||||||
|
"migration-finished-with-caveat-title": "Resource migration complete",
|
||||||
|
"migration-finished-with-errors-body": "The migration has completed, but some items could not be migrated to the cloud stack. Check the failed resources for more details",
|
||||||
|
"migration-finished-with-warnings-body": "The migration has completed with some warnings. Check individual resources for more details",
|
||||||
"snapshot-error-status-body": "There was an error creating the snapshot or starting the migration process. See the Grafana server logs for more details",
|
"snapshot-error-status-body": "There was an error creating the snapshot or starting the migration process. See the Grafana server logs for more details",
|
||||||
"snapshot-error-status-title": "Error migrating resources",
|
"snapshot-error-status-title": "Error migrating resources",
|
||||||
"some-resources-errored-body": "The migration has completed, but some items could not be migrated to the cloud stack. Check the failed resources for more details",
|
|
||||||
"some-resources-errored-title": "Resource migration complete",
|
|
||||||
"upload-snapshot-error-title": "Error uploading snapshot"
|
"upload-snapshot-error-title": "Error uploading snapshot"
|
||||||
},
|
},
|
||||||
"pdc": {
|
"pdc": {
|
||||||
@@ -1136,12 +1137,14 @@
|
|||||||
"message": "No SLAs are available yet. <2>Visit our docs</2> to learn more about this feature!",
|
"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"
|
"title": "Migrate to Grafana Cloud is in public preview"
|
||||||
},
|
},
|
||||||
"resource-error": {
|
"resource-details": {
|
||||||
"dismiss-button": "OK",
|
"dismiss-button": "OK",
|
||||||
|
"error-title": "Unable to migrate this resource:",
|
||||||
|
"generic-title": "Resource migration details:",
|
||||||
|
"missing-message": "No message provided.",
|
||||||
"resource-summary": "{{refId}} ({{typeName}})",
|
"resource-summary": "{{refId}} ({{typeName}})",
|
||||||
"specific-error": "The specific error was:",
|
"title": "Migration resource details",
|
||||||
"title": "Unable to migrate this resource",
|
"warning-title": "Resource migrated with a warning:"
|
||||||
"unknown-error": "An unknown error occurred."
|
|
||||||
},
|
},
|
||||||
"resource-status": {
|
"resource-status": {
|
||||||
"error-details-button": "Details",
|
"error-details-button": "Details",
|
||||||
@@ -1149,7 +1152,9 @@
|
|||||||
"migrated": "Uploaded to cloud",
|
"migrated": "Uploaded to cloud",
|
||||||
"migrating": "Uploading...",
|
"migrating": "Uploading...",
|
||||||
"not-migrated": "Not yet uploaded",
|
"not-migrated": "Not yet uploaded",
|
||||||
"unknown": "Unknown"
|
"unknown": "Unknown",
|
||||||
|
"warning": "Uploaded with warning",
|
||||||
|
"warning-details-button": "Details"
|
||||||
},
|
},
|
||||||
"resource-table": {
|
"resource-table": {
|
||||||
"unknown-datasource-title": "Data source {{datasourceUID}}",
|
"unknown-datasource-title": "Data source {{datasourceUID}}",
|
||||||
|
|||||||
@@ -1115,10 +1115,11 @@
|
|||||||
"error-see-server-logs": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
|
"error-see-server-logs": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
|
||||||
"get-session-error-title": "Ēřřőř ľőäđįʼnģ mįģřäŧįőʼn čőʼnƒįģūřäŧįőʼn",
|
"get-session-error-title": "Ēřřőř ľőäđįʼnģ mįģřäŧįőʼn čőʼnƒįģūřäŧįőʼn",
|
||||||
"get-snapshot-error-title": "Ēřřőř ľőäđįʼnģ şʼnäpşĥőŧ",
|
"get-snapshot-error-title": "Ēřřőř ľőäđįʼnģ şʼnäpşĥőŧ",
|
||||||
|
"migration-finished-with-caveat-title": "Ŗęşőūřčę mįģřäŧįőʼn čőmpľęŧę",
|
||||||
|
"migration-finished-with-errors-body": "Ŧĥę mįģřäŧįőʼn ĥäş čőmpľęŧęđ, þūŧ şőmę įŧęmş čőūľđ ʼnőŧ þę mįģřäŧęđ ŧő ŧĥę čľőūđ şŧäčĸ. Cĥęčĸ ŧĥę ƒäįľęđ řęşőūřčęş ƒőř mőřę đęŧäįľş",
|
||||||
|
"migration-finished-with-warnings-body": "Ŧĥę mįģřäŧįőʼn ĥäş čőmpľęŧęđ ŵįŧĥ şőmę ŵäřʼnįʼnģş. Cĥęčĸ įʼnđįvįđūäľ řęşőūřčęş ƒőř mőřę đęŧäįľş",
|
||||||
"snapshot-error-status-body": "Ŧĥęřę ŵäş äʼn ęřřőř čřęäŧįʼnģ ŧĥę şʼnäpşĥőŧ őř şŧäřŧįʼnģ ŧĥę mįģřäŧįőʼn přőčęşş. Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
|
"snapshot-error-status-body": "Ŧĥęřę ŵäş äʼn ęřřőř čřęäŧįʼnģ ŧĥę şʼnäpşĥőŧ őř şŧäřŧįʼnģ ŧĥę mįģřäŧįőʼn přőčęşş. Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş",
|
||||||
"snapshot-error-status-title": "Ēřřőř mįģřäŧįʼnģ řęşőūřčęş",
|
"snapshot-error-status-title": "Ēřřőř mįģřäŧįʼnģ řęşőūřčęş",
|
||||||
"some-resources-errored-body": "Ŧĥę mįģřäŧįőʼn ĥäş čőmpľęŧęđ, þūŧ şőmę įŧęmş čőūľđ ʼnőŧ þę mįģřäŧęđ ŧő ŧĥę čľőūđ şŧäčĸ. Cĥęčĸ ŧĥę ƒäįľęđ řęşőūřčęş ƒőř mőřę đęŧäįľş",
|
|
||||||
"some-resources-errored-title": "Ŗęşőūřčę mįģřäŧįőʼn čőmpľęŧę",
|
|
||||||
"upload-snapshot-error-title": "Ēřřőř ūpľőäđįʼnģ şʼnäpşĥőŧ"
|
"upload-snapshot-error-title": "Ēřřőř ūpľőäđįʼnģ şʼnäpşĥőŧ"
|
||||||
},
|
},
|
||||||
"pdc": {
|
"pdc": {
|
||||||
@@ -1136,12 +1137,14 @@
|
|||||||
"message": "Ńő ŜĿÅş äřę äväįľäþľę yęŧ. <2>Vįşįŧ őūř đőčş</2> ŧő ľęäřʼn mőřę äþőūŧ ŧĥįş ƒęäŧūřę!",
|
"message": "Ńő ŜĿÅş äřę äväįľäþľę yęŧ. <2>Vįşįŧ őūř đőčş</2> ŧő ľęäřʼn mőřę äþőūŧ ŧĥįş ƒęäŧūřę!",
|
||||||
"title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ"
|
"title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ"
|
||||||
},
|
},
|
||||||
"resource-error": {
|
"resource-details": {
|
||||||
"dismiss-button": "ØĶ",
|
"dismiss-button": "ØĶ",
|
||||||
|
"error-title": "Ůʼnäþľę ŧő mįģřäŧę ŧĥįş řęşőūřčę:",
|
||||||
|
"generic-title": "Ŗęşőūřčę mįģřäŧįőʼn đęŧäįľş:",
|
||||||
|
"missing-message": "Ńő męşşäģę přővįđęđ.",
|
||||||
"resource-summary": "{{refId}} ({{typeName}})",
|
"resource-summary": "{{refId}} ({{typeName}})",
|
||||||
"specific-error": "Ŧĥę şpęčįƒįč ęřřőř ŵäş:",
|
"title": "Mįģřäŧįőʼn řęşőūřčę đęŧäįľş",
|
||||||
"title": "Ůʼnäþľę ŧő mįģřäŧę ŧĥįş řęşőūřčę",
|
"warning-title": "Ŗęşőūřčę mįģřäŧęđ ŵįŧĥ ä ŵäřʼnįʼnģ:"
|
||||||
"unknown-error": "Åʼn ūʼnĸʼnőŵʼn ęřřőř őččūřřęđ."
|
|
||||||
},
|
},
|
||||||
"resource-status": {
|
"resource-status": {
|
||||||
"error-details-button": "Đęŧäįľş",
|
"error-details-button": "Đęŧäįľş",
|
||||||
@@ -1149,7 +1152,9 @@
|
|||||||
"migrated": "Ůpľőäđęđ ŧő čľőūđ",
|
"migrated": "Ůpľőäđęđ ŧő čľőūđ",
|
||||||
"migrating": "Ůpľőäđįʼnģ...",
|
"migrating": "Ůpľőäđįʼnģ...",
|
||||||
"not-migrated": "Ńőŧ yęŧ ūpľőäđęđ",
|
"not-migrated": "Ńőŧ yęŧ ūpľőäđęđ",
|
||||||
"unknown": "Ůʼnĸʼnőŵʼn"
|
"unknown": "Ůʼnĸʼnőŵʼn",
|
||||||
|
"warning": "Ůpľőäđęđ ŵįŧĥ ŵäřʼnįʼnģ",
|
||||||
|
"warning-details-button": "Đęŧäįľş"
|
||||||
},
|
},
|
||||||
"resource-table": {
|
"resource-table": {
|
||||||
"unknown-datasource-title": "Đäŧä şőūřčę {{datasourceUID}}",
|
"unknown-datasource-title": "Đäŧä şőūřčę {{datasourceUID}}",
|
||||||
|
|||||||
@@ -7046,7 +7046,7 @@
|
|||||||
},
|
},
|
||||||
"MigrateDataResponseItemDTO": {
|
"MigrateDataResponseItemDTO": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"error": {
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"refId": {
|
"refId": {
|
||||||
@@ -7055,6 +7055,7 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"OK",
|
"OK",
|
||||||
|
"WARNING",
|
||||||
"ERROR",
|
"ERROR",
|
||||||
"PENDING",
|
"PENDING",
|
||||||
"UNKNOWN"
|
"UNKNOWN"
|
||||||
|
|||||||
Reference in New Issue
Block a user