diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index d946e3d3103..312a1a7ed16 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -435,10 +435,10 @@ func (cma *CloudMigrationAPI) GetSnapshot(c *contextmodel.ReqContext) response.R dtoResults := make([]MigrateDataResponseItemDTO, len(results)) for i := 0; i < len(results); i++ { dtoResults[i] = MigrateDataResponseItemDTO{ - Type: MigrateDataType(results[i].Type), - RefID: results[i].RefID, - Status: ItemStatus(results[i].Status), - Error: results[i].Error, + Type: MigrateDataType(results[i].Type), + RefID: results[i].RefID, + Status: ItemStatus(results[i].Status), + Message: results[i].Error, } } diff --git a/pkg/services/cloudmigration/api/api_test.go b/pkg/services/cloudmigration/api/api_test.go index bf0cbc4f041..10b65fb28a3 100644 --- a/pkg/services/cloudmigration/api/api_test.go +++ b/pkg/services/cloudmigration/api/api_test.go @@ -261,7 +261,7 @@ func TestCloudMigrationAPI_RunMigration(t *testing.T) { requestUrl: "/api/cloudmigration/migration/1234/run", basicRole: org.RoleAdmin, 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", @@ -303,7 +303,7 @@ func TestCloudMigrationAPI_GetMigrationRun(t *testing.T) { requestUrl: "/api/cloudmigration/migration/run/1234", basicRole: org.RoleAdmin, 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", diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index aef06a817f4..a8c74f2508e 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -111,8 +111,8 @@ type MigrateDataResponseItemDTO struct { // required:true RefID string `json:"refId"` // required:true - Status ItemStatus `json:"status"` - Error string `json:"error,omitempty"` + Status ItemStatus `json:"status"` + Message string `json:"message,omitempty"` } // swagger:enum MigrateDataType @@ -129,6 +129,7 @@ type ItemStatus string const ( ItemStatusOK ItemStatus = "OK" + ItemStatusWarning ItemStatus = "WARNING" ItemStatusError ItemStatus = "ERROR" ItemStatusPending ItemStatus = "PENDING" ItemStatusUnknown ItemStatus = "UNKNOWN" @@ -192,10 +193,10 @@ func convertMigrateDataResponseToDTO(r cloudmigration.MigrateDataResponse) Migra for i := 0; i < len(r.Items); i++ { item := r.Items[i] items[i] = MigrateDataResponseItemDTO{ - Type: MigrateDataType(item.Type), - RefID: item.RefID, - Status: ItemStatus(item.Status), - Error: item.Error, + Type: MigrateDataType(item.Type), + RefID: item.RefID, + Status: ItemStatus(item.Status), + Message: item.Error, } } return MigrateDataResponseDTO{ diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index 36763dfffe4..7ef3d952c40 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/gcom" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/secrets" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/user" @@ -57,6 +58,7 @@ type Service struct { gcomService gcom.Service dashboardService dashboards.DashboardService folderService folder.Service + pluginStore pluginstore.Store secretsService secrets.Service kvStore *kvstore.NamespacedKVStore @@ -90,6 +92,7 @@ func ProvideService( tracer tracing.Tracer, dashboardService dashboards.DashboardService, folderService folder.Service, + pluginStore pluginstore.Store, kvStore kvstore.KVStore, ) (cloudmigration.Service, error) { if !features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) { @@ -107,6 +110,7 @@ func ProvideService( secretsService: secretsService, dashboardService: dashboardService, folderService: folderService, + pluginStore: pluginStore, kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"), } s.api = api.RegisterApi(routeRegister, s, tracer) @@ -590,12 +594,19 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps 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 if err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{ UID: snapshot.UID, SessionID: sessionUid, Status: localStatus, - Resources: snapshotMeta.Results, + Resources: resources, }); err != nil { 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 } + +// 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 +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 006a862da36..38bd7605ddd 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" "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/gmsclient" "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/folder" "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" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/user" @@ -399,6 +401,122 @@ 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 + 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 { c := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{OrgID: 1}, @@ -461,6 +579,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi tracer, dashboardService, mockFolder, + &pluginstore.FakePluginStore{}, kvstore.ProvideService(sqlStore), ) require.NoError(t, err) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index a22ea2f0dff..0bd17ea06e2 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -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 // 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 { - // 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 { 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 { @@ -314,6 +309,8 @@ func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotU return err } else if n == 0 { 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) if err != nil { return err diff --git a/pkg/services/cloudmigration/gmsclient/inmemory_client.go b/pkg/services/cloudmigration/gmsclient/inmemory_client.go index 2a9d73e0b05..74cb326382d 100644 --- a/pkg/services/cloudmigration/gmsclient/inmemory_client.go +++ b/pkg/services/cloudmigration/gmsclient/inmemory_client.go @@ -105,6 +105,12 @@ func (c *memoryClientImpl) GetSnapshotStatus(ctx context.Context, session cloudm RefID: "folder1", 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.", + }, }, } diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 6baf0f4c3fa..0ea1b8b7525 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -88,6 +88,7 @@ type ItemStatus string const ( ItemStatusOK ItemStatus = "OK" + ItemStatusWarning ItemStatus = "WARNING" ItemStatusError ItemStatus = "ERROR" ItemStatusPending ItemStatus = "PENDING" ) diff --git a/public/api-merged.json b/public/api-merged.json index 8522f35233c..577d6ed5f3e 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -16974,7 +16974,7 @@ "status" ], "properties": { - "error": { + "message": { "type": "string" }, "refId": { @@ -16984,6 +16984,7 @@ "type": "string", "enum": [ "OK", + "WARNING", "ERROR", "PENDING", "UNKNOWN" diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index 890138028b4..c5eebb2a30a 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -154,9 +154,9 @@ export type CreateSnapshotResponseDto = { uid?: string; }; export type MigrateDataResponseItemDto = { - error?: string; + message?: string; refId: string; - status: 'OK' | 'ERROR' | 'PENDING' | 'UNKNOWN'; + status: 'OK' | 'WARNING' | 'ERROR' | 'PENDING' | 'UNKNOWN'; type: 'DASHBOARD' | 'DATASOURCE' | 'FOLDER'; }; export type SnapshotResourceStats = { diff --git a/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx index 02fa15f267e..cacc3fc2113 100644 --- a/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx +++ b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx @@ -52,6 +52,7 @@ export function MigrationSummary(props: MigrationSummaryProps) { const totalCount = snapshot?.stats?.total ?? 0; const errorCount = snapshot?.stats?.statuses?.['ERROR'] ?? 0; const successCount = snapshot?.stats?.statuses?.['OK'] ?? 0; + const warningCount = snapshot?.stats?.statuses?.['WARNING'] ?? 0; return ( - {successCount} + {successCount + warningCount} diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx index 823b4a5e99a..00b8d0d522d 100644 --- a/public/app/features/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx @@ -351,14 +351,27 @@ function getError(props: GetErrorProps): ErrorDescription | undefined { } 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 { severity: 'warning', - title: t('migrate-to-cloud.onprem.some-resources-errored-title', 'Resource migration complete'), - body: t( - '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' - ), + title: t('migrate-to-cloud.onprem.migration-finished-with-caveat-title', 'Resource migration complete'), + body: msgBody, }; } diff --git a/public/app/features/migrate-to-cloud/onprem/ResourceErrorModal.tsx b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx similarity index 50% rename from public/app/features/migrate-to-cloud/onprem/ResourceErrorModal.tsx rename to public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx index 9c95e441c16..adc673b9ecd 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourceErrorModal.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx @@ -4,49 +4,54 @@ import { Trans, t } from 'app/core/internationalization'; import { prettyTypeName } from './TypeCell'; import { ResourceTableItem } from './types'; -interface ResourceErrorModalProps { +interface ResourceDetailsModalProps { resource: ResourceTableItem | undefined; onClose: () => void; } -export function ResourceErrorModal(props: ResourceErrorModalProps) { +export function ResourceDetailsModal(props: ResourceDetailsModalProps) { const { resource, onClose } = props; const refId = resource?.refId; 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 ( {resource && ( - + {{ refId }} ({{ typeName }}) - {resource.error ? ( + {resource.message ? ( <> - - The specific error was: - + {msgTitle} - {resource.error} + {resource.message} ) : ( - An unknown error occurred. + No message provided. )} )} diff --git a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx index 9e7d5778c8f..2b5d212cbde 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.test.tsx @@ -99,7 +99,7 @@ describe('ResourcesTable', () => { expect(screen.getByText('Uploaded to cloud')).toBeInTheDocument(); }); - it('renders the success error correctly', () => { + it('renders the error status correctly', () => { const resources = [ wellFormedDatasourceMigrationItem(1, { refId: datasourceA.uid, @@ -112,12 +112,43 @@ describe('ResourcesTable', () => { 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 = [ wellFormedDatasourceMigrationItem(1, { refId: datasourceA.uid, 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', }), ]; diff --git a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx index 01e7e464fa3..7fe8e828d4c 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourcesTable.tsx @@ -5,7 +5,7 @@ import { InteractiveTable, Pagination, Stack } from '@grafana/ui'; import { MigrateDataResponseItemDto } from '../api'; import { NameCell } from './NameCell'; -import { ResourceErrorModal } from './ResourceErrorModal'; +import { ResourceDetailsModal } from './ResourceDetailsModal'; import { StatusCell } from './StatusCell'; import { TypeCell } from './TypeCell'; import { ResourceTableItem } from './types'; @@ -24,15 +24,15 @@ const columns = [ ]; export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, page = 1 }: ResourcesTableProps) { - const [erroredResource, setErroredResource] = useState(); + const [focusedResource, setfocusedResource] = useState(); - const handleShowErrorModal = useCallback((resource: ResourceTableItem) => { - setErroredResource(resource); + const handleShowDetailsModal = useCallback((resource: ResourceTableItem) => { + setfocusedResource(resource); }, []); const data = useMemo(() => { - return resources.map((r) => ({ ...r, showError: handleShowErrorModal })); - }, [resources, handleShowErrorModal]); + return resources.map((r) => ({ ...r, showDetails: handleShowDetailsModal })); + }, [resources, handleShowDetailsModal]); return ( <> @@ -42,7 +42,7 @@ export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, pag - setErroredResource(undefined)} /> + setfocusedResource(undefined)} /> ); } diff --git a/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx b/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx index caf0c7b82fc..714e611fc19 100644 --- a/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/StatusCell.tsx @@ -13,6 +13,8 @@ export function StatusCell(props: CellProps) { return {t('migrate-to-cloud.resource-status.not-migrated', 'Not yet uploaded')}; } else if (item.status === 'OK') { return {t('migrate-to-cloud.resource-status.migrated', 'Uploaded to cloud')}; + } else if (item.status === 'WARNING') { + return ; } else if (item.status === 'ERROR') { return ; } @@ -25,11 +27,25 @@ function ErrorCell({ item }: { item: ResourceTableItem }) { {t('migrate-to-cloud.resource-status.failed', 'Error')} - {item.error && ( - )} ); } + +function WarningCell({ item }: { item: ResourceTableItem }) { + return ( + + {t('migrate-to-cloud.resource-status.warning', 'Uploaded with warning')} + + {item.message && ( + + )} + + ); +} diff --git a/public/app/features/migrate-to-cloud/onprem/types.ts b/public/app/features/migrate-to-cloud/onprem/types.ts index 63e1979dd71..a629905d7c6 100644 --- a/public/app/features/migrate-to-cloud/onprem/types.ts +++ b/public/app/features/migrate-to-cloud/onprem/types.ts @@ -1,5 +1,5 @@ import { MigrateDataResponseItemDto } from '../api'; export interface ResourceTableItem extends MigrateDataResponseItemDto { - showError: (resource: ResourceTableItem) => void; + showDetails: (resource: ResourceTableItem) => void; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7d13fe8b4b0..ebb53e1ea5b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1115,10 +1115,11 @@ "error-see-server-logs": "See the Grafana server logs for more details", "get-session-error-title": "Error loading migration configuration", "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-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" }, "pdc": { @@ -1136,12 +1137,14 @@ "message": "No SLAs are available yet. <2>Visit our docs to learn more about this feature!", "title": "Migrate to Grafana Cloud is in public preview" }, - "resource-error": { + "resource-details": { "dismiss-button": "OK", + "error-title": "Unable to migrate this resource:", + "generic-title": "Resource migration details:", + "missing-message": "No message provided.", "resource-summary": "{{refId}} ({{typeName}})", - "specific-error": "The specific error was:", - "title": "Unable to migrate this resource", - "unknown-error": "An unknown error occurred." + "title": "Migration resource details", + "warning-title": "Resource migrated with a warning:" }, "resource-status": { "error-details-button": "Details", @@ -1149,7 +1152,9 @@ "migrated": "Uploaded to cloud", "migrating": "Uploading...", "not-migrated": "Not yet uploaded", - "unknown": "Unknown" + "unknown": "Unknown", + "warning": "Uploaded with warning", + "warning-details-button": "Details" }, "resource-table": { "unknown-datasource-title": "Data source {{datasourceUID}}", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 7b5721517b6..a721e58ce1f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1115,10 +1115,11 @@ "error-see-server-logs": "Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş", "get-session-error-title": "Ēřřőř ľőäđįʼnģ mįģřäŧįőʼn čőʼnƒįģūřäŧįőʼn", "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-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şĥőŧ" }, "pdc": { @@ -1136,12 +1137,14 @@ "message": "Ńő ŜĿÅş äřę äväįľäþľę yęŧ. <2>Vįşįŧ őūř đőčş ŧő ľęäřʼn mőřę äþőūŧ ŧĥįş ƒęäŧūřę!", "title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ" }, - "resource-error": { + "resource-details": { "dismiss-button": "ØĶ", + "error-title": "Ůʼnäþľę ŧő mįģřäŧę ŧĥįş řęşőūřčę:", + "generic-title": "Ŗęşőūřčę mįģřäŧįőʼn đęŧäįľş:", + "missing-message": "Ńő męşşäģę přővįđęđ.", "resource-summary": "{{refId}} ({{typeName}})", - "specific-error": "Ŧĥę şpęčįƒįč ęřřőř ŵäş:", - "title": "Ůʼnäþľę ŧő mįģřäŧę ŧĥįş řęşőūřčę", - "unknown-error": "Åʼn ūʼnĸʼnőŵʼn ęřřőř őččūřřęđ." + "title": "Mįģřäŧįőʼn řęşőūřčę đęŧäįľş", + "warning-title": "Ŗęşőūřčę mįģřäŧęđ ŵįŧĥ ä ŵäřʼnįʼnģ:" }, "resource-status": { "error-details-button": "Đęŧäįľş", @@ -1149,7 +1152,9 @@ "migrated": "Ůpľőäđęđ ŧő čľőūđ", "migrating": "Ůpľőäđįʼnģ...", "not-migrated": "Ńőŧ yęŧ ūpľőäđęđ", - "unknown": "Ůʼnĸʼnőŵʼn" + "unknown": "Ůʼnĸʼnőŵʼn", + "warning": "Ůpľőäđęđ ŵįŧĥ ŵäřʼnįʼnģ", + "warning-details-button": "Đęŧäįľş" }, "resource-table": { "unknown-datasource-title": "Đäŧä şőūřčę {{datasourceUID}}", diff --git a/public/openapi3.json b/public/openapi3.json index b51b79aa969..927e7fed22a 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7046,7 +7046,7 @@ }, "MigrateDataResponseItemDTO": { "properties": { - "error": { + "message": { "type": "string" }, "refId": { @@ -7055,6 +7055,7 @@ "status": { "enum": [ "OK", + "WARNING", "ERROR", "PENDING", "UNKNOWN"