CloudMigrations: Add Cypress happy path test case scenarios (#103250)
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
import { e2e } from '../utils';
|
||||
|
||||
describe.skip('Migrate to Cloud (On-prem)', () => {
|
||||
// Here we are mostly testing the UI flow and can do interesting things with the backend responses to see how the UI behaves.
|
||||
describe('with mocked calls to the API backend', () => {
|
||||
afterEach(() => {
|
||||
cy.get('[data-testid="migrate-to-cloud-summary-disconnect-button"]').should('be.visible').click();
|
||||
});
|
||||
|
||||
const SESSION_UID = 'fehq6hqd246iox';
|
||||
const SNAPSHOT_UID1 = 'cehq6vdjqbqbkx';
|
||||
|
||||
const SNAPSHOT_RESULTS = [
|
||||
{ name: 'FolderA', type: 'FOLDER', refId: 'ref-id-folder-a', parentName: 'General' },
|
||||
{ name: 'FolderB', type: 'FOLDER', refId: 'ref-id-folder-b', parentName: 'General' },
|
||||
{ name: 'Prometheus', type: 'DATASOURCE', refId: 'prometheus' },
|
||||
{ name: 'Postgres', type: 'DATASOURCE', refId: 'postgres' },
|
||||
{ name: 'Loki', type: 'DATASOURCE', refId: 'loki' },
|
||||
{ name: 'Alert Rule A', type: 'ALERT_RULE', refId: 'alert-rule-a', parentName: 'FolderA' },
|
||||
{ name: 'Alert Rule B', type: 'ALERT_RULE', refId: 'alert-rule-b', parentName: 'FolderB' },
|
||||
{ name: 'Alert Rule C', type: 'ALERT_RULE', refId: 'alert-rule-c', parentName: 'FolderB' },
|
||||
{ name: 'Alert Rule Group A', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-a', parentName: 'FolderA' },
|
||||
{ name: 'Alert Rule Group B', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-b', parentName: 'FolderB' },
|
||||
{ name: 'Contact Point A', type: 'CONTACT_POINT', refId: 'contact-point-a' },
|
||||
{ name: 'Contact Point B', type: 'CONTACT_POINT', refId: 'contact-point-b' },
|
||||
{ name: 'Contact Point C', type: 'CONTACT_POINT', refId: 'contact-point-c' },
|
||||
{ name: 'Notification Policy A', type: 'NOTIFICATION_POLICY', refId: 'notification-policy-a' },
|
||||
{ name: 'Notification Template A', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-a' },
|
||||
{ name: 'Notification Template B', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-b' },
|
||||
{ name: 'Notification Template C', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-c' },
|
||||
{ name: 'Plugin A', type: 'PLUGIN', refId: 'plugin-a' },
|
||||
{ name: 'Plugin B', type: 'PLUGIN', refId: 'plugin-b' },
|
||||
{ name: 'Plugin C', type: 'PLUGIN', refId: 'plugin-c' },
|
||||
{ name: 'Mute Timing A', type: 'MUTE_TIMING', refId: 'mute-timing-a' },
|
||||
{ name: 'Mute Timing B', type: 'MUTE_TIMING', refId: 'mute-timing-b' },
|
||||
];
|
||||
|
||||
const MIGRATION_SESSION = {
|
||||
uid: SESSION_UID,
|
||||
slug: 'test-slug',
|
||||
created: '2025-04-02T21:36:08+02:00',
|
||||
updated: '2025-04-02T21:36:08+02:00',
|
||||
};
|
||||
|
||||
const STATS = {
|
||||
types: SNAPSHOT_RESULTS.reduce(
|
||||
(acc, r) => {
|
||||
acc[r.type] = (acc[r.type] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
),
|
||||
statuses: {
|
||||
PENDING: SNAPSHOT_RESULTS.length,
|
||||
},
|
||||
total: SNAPSHOT_RESULTS.length,
|
||||
};
|
||||
|
||||
it('creates and uploads a snapshot sucessfully', () => {
|
||||
// Login using the UI.
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
|
||||
|
||||
// Visit the migrate to cloud onprem page.
|
||||
e2e.pages.MigrateToCloud.visit();
|
||||
|
||||
// Open the connect modal and enter the token.
|
||||
cy.get('[data-testid="migrate-to-cloud-connect-session-modal-button"]').should('be.visible').click();
|
||||
|
||||
cy.get('[data-testid="migrate-to-cloud-connect-session-modal-token-input"]')
|
||||
.should('be.visible')
|
||||
.focus()
|
||||
.type('test');
|
||||
|
||||
cy.intercept('POST', '/api/cloudmigration/migration', {
|
||||
statusCode: 200,
|
||||
body: MIGRATION_SESSION,
|
||||
}).as('createMigrationToken');
|
||||
|
||||
cy.intercept('GET', '/api/cloudmigration/migration', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
sessions: [MIGRATION_SESSION],
|
||||
},
|
||||
}).as('getMigrationSessionList');
|
||||
|
||||
cy.intercept('GET', `/api/cloudmigration/migration/${SESSION_UID}/snapshots?page=1&limit=1*`, {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
snapshots: [],
|
||||
},
|
||||
}).as('getSnapshotListInitial');
|
||||
|
||||
// Click the connect button to create the token.
|
||||
cy.get('[data-testid="migrate-to-cloud-connect-session-modal-connect-button"]').should('be.visible').click();
|
||||
|
||||
// Wait for the token to be created and the migration session list to be fetched to kickstart the UI state machine.
|
||||
cy.wait(['@createMigrationToken', '@getMigrationSessionList', '@getSnapshotListInitial']);
|
||||
|
||||
cy.intercept('POST', `/api/cloudmigration/migration/${SESSION_UID}/snapshot`, {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
uid: SNAPSHOT_UID1,
|
||||
},
|
||||
}).as('createSnapshot');
|
||||
|
||||
cy.intercept('GET', `/api/cloudmigration/migration/${SESSION_UID}/snapshots?page=1&limit=1*`, {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
snapshots: [
|
||||
{
|
||||
uid: SNAPSHOT_UID1,
|
||||
sessionUid: SESSION_UID,
|
||||
status: 'CREATING',
|
||||
created: '2025-04-02T21:40:23+02:00',
|
||||
finished: '0001-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
}).as('getSnapshotListCreating');
|
||||
|
||||
let getSnapshotCalled = false;
|
||||
cy.intercept(
|
||||
'GET',
|
||||
`/api/cloudmigration/migration/${SESSION_UID}/snapshot/${SNAPSHOT_UID1}?resultPage=1&resultLimit=50`,
|
||||
(req) => {
|
||||
if (!getSnapshotCalled) {
|
||||
getSnapshotCalled = true;
|
||||
req.reply((res) => {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
body: {
|
||||
uid: SNAPSHOT_UID1,
|
||||
sessionUid: SESSION_UID,
|
||||
status: 'CREATING',
|
||||
created: '2025-04-02T21:40:23+02:00',
|
||||
finished: '0001-01-01T00:00:00Z',
|
||||
results: [],
|
||||
stats: {
|
||||
types: {},
|
||||
statuses: {},
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
req.reply((res) => {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
body: {
|
||||
uid: SNAPSHOT_UID1,
|
||||
sessionUid: SESSION_UID,
|
||||
status: 'PENDING_UPLOAD',
|
||||
created: '2025-04-02T21:40:23+02:00',
|
||||
finished: '0001-01-01T00:00:00Z',
|
||||
results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })),
|
||||
stats: STATS,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
).as('getSnapshot');
|
||||
|
||||
// Build the snapshot.
|
||||
cy.get('[data-testid="migrate-to-cloud-configure-snapshot-build-snapshot-button"]').should('be.visible').click();
|
||||
|
||||
// Wait for the snapshot to be created. Simulate it going from INITIALIZING to PENDING_UPLOAD.
|
||||
cy.wait(['@createSnapshot', '@getSnapshotListCreating', '@getSnapshot']);
|
||||
|
||||
cy.intercept('POST', `/api/cloudmigration/migration/${SESSION_UID}/snapshot/${SNAPSHOT_UID1}/upload`, {
|
||||
statusCode: 200,
|
||||
}).as('uploadSnapshot');
|
||||
|
||||
cy.intercept('GET', `/api/cloudmigration/migration/${SESSION_UID}/snapshots?page=1&limit=1*`, {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
snapshots: [
|
||||
{
|
||||
uid: SNAPSHOT_UID1,
|
||||
sessionUid: SESSION_UID,
|
||||
status: 'UPLOADING',
|
||||
created: '2025-04-02T21:40:23+02:00',
|
||||
finished: '0001-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
}).as('getSnapshotListUploading');
|
||||
|
||||
// Upload the snapshot.
|
||||
cy.get('[data-testid="migrate-to-cloud-summary-upload-snapshot-button"]').should('be.visible').click();
|
||||
|
||||
// Simulate the snapshot being uploaded, the frontend will keep polling until the snapshot is either finished or errored.
|
||||
let getSnapshotUploadingCalls = 0;
|
||||
cy.intercept(
|
||||
'GET',
|
||||
`/api/cloudmigration/migration/${SESSION_UID}/snapshot/${SNAPSHOT_UID1}?resultPage=1&resultLimit=50`,
|
||||
(req) => {
|
||||
req.reply((res) => {
|
||||
if (getSnapshotUploadingCalls <= 1) {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
body: {
|
||||
uid: SNAPSHOT_UID1,
|
||||
sessionUid: SESSION_UID,
|
||||
status: getSnapshotUploadingCalls === 1 ? 'PROCESSING' : 'UPLOADING',
|
||||
created: '2025-04-02T21:40:23+02:00',
|
||||
finished: '0001-01-01T00:00:00Z',
|
||||
results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })),
|
||||
stats: STATS,
|
||||
},
|
||||
});
|
||||
getSnapshotUploadingCalls++;
|
||||
} else {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
body: {
|
||||
uid: SNAPSHOT_UID1,
|
||||
sessionUid: SESSION_UID,
|
||||
status: 'FINISHED',
|
||||
created: '2025-03-27T12:00:00Z',
|
||||
finished: '2025-03-27T12:00:00Z',
|
||||
results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'OK' })),
|
||||
stats: {
|
||||
types: STATS.types,
|
||||
statuses: SNAPSHOT_RESULTS.reduce(
|
||||
(acc, r) => {
|
||||
const status = (r as { status?: string }).status || 'UNKNOWN';
|
||||
acc[status] = (acc[status] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
),
|
||||
total: SNAPSHOT_RESULTS.length,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
).as('getSnapshotUploading');
|
||||
|
||||
// Wait for the request to kickstart the upload and then wait until it is finished.
|
||||
cy.wait(['@uploadSnapshot', '@getSnapshotListUploading', '@getSnapshotUploading']);
|
||||
|
||||
// The upload button should now be hidden away.
|
||||
cy.get('[data-testid="migrate-to-cloud-summary-upload-snapshot-button"]').should('be.disabled');
|
||||
|
||||
// And the rebuild button should be visible.
|
||||
cy.get('[data-testid="migrate-to-cloud-summary-rebuild-snapshot-button"]').should('be.visible');
|
||||
|
||||
// At least some of the items are marked with "Uploaded to cloud" status.
|
||||
cy.contains('Uploaded to cloud').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
// Here we are doing a more black box testing of the migration flow, without explicitly mocking the API calls,
|
||||
// but we instead rely on the `[cloud_migration] developer_mode = true` to be set in the `custom.ini` file,
|
||||
// which will make the service use in-memory fake implementations of 3rdparty dependencies, but we'll still
|
||||
// use the real API endpoints, database and business logic.
|
||||
describe('with a fake GMS backend implementation', () => {
|
||||
afterEach(() => {
|
||||
cy.get('[data-testid="migrate-to-cloud-summary-disconnect-button"]').should('be.visible').click();
|
||||
});
|
||||
|
||||
// Manually crafted base64 token for testing, does not contain any sensitive data.
|
||||
const TEST_TOKEN =
|
||||
'eyJUb2tlbiI6ImdsY19kZXZfZXlKdklqb2lNVEl6TkNJc0ltNGlPaUpuY21GbVlXNWhMV05zYjNWa0xXMXBaM0poZEdsdmJuTXRNVEl6TkNJc0ltc2lPaUowWlhOMElpd2liU0k2ZXlKeUlqb2laR1YyTFhWekxXTmxiblJ5WVd3aWZYMEsiLCJJbnN0YW5jZSI6eyJTdGFja0lEIjoxMjM0LCJTbHVnIjoidGVzdC1zbHVnIiwiUmVnaW9uU2x1ZyI6ImRldi11cy1jZW50cmFsIiwiQ2x1c3RlclNsdWciOiJkZXYtdXMtY2VudHJhbC0wIn19Cg==';
|
||||
|
||||
it('creates a snapshot sucessfully', () => {
|
||||
// Login using the UI.
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
|
||||
|
||||
// Visit the migrate to cloud onprem page.
|
||||
e2e.pages.MigrateToCloud.visit();
|
||||
|
||||
// Open the connect modal and enter the token.
|
||||
cy.get('[data-testid="migrate-to-cloud-connect-session-modal-button"]').should('be.visible').click();
|
||||
|
||||
cy.get('[data-testid="migrate-to-cloud-connect-session-modal-token-input"]')
|
||||
.should('be.visible')
|
||||
.focus()
|
||||
.type(TEST_TOKEN);
|
||||
|
||||
// Click the connect button to create the token.
|
||||
cy.get('[data-testid="migrate-to-cloud-connect-session-modal-connect-button"]').should('be.visible').click();
|
||||
|
||||
// Build the snapshot.
|
||||
cy.get('[data-testid="migrate-to-cloud-configure-snapshot-build-snapshot-button"]').should('be.visible').click();
|
||||
|
||||
// And the rebuild button should be visible.
|
||||
cy.get('[data-testid="migrate-to-cloud-summary-rebuild-snapshot-button"]').should('be.visible');
|
||||
|
||||
// We don't upload the snapshot yet because we need to create a mock server to validate the uploaded items,
|
||||
// similarly to what the SMTP (tester) server does.
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1049,6 +1049,11 @@ export const versionedPages = {
|
||||
[MIN_GRAFANA_VERSION]: (pluginId: string) => `/plugins/${pluginId}`,
|
||||
},
|
||||
},
|
||||
MigrateToCloud: {
|
||||
url: {
|
||||
'11.2.0': '/admin/migrate-to-cloud',
|
||||
},
|
||||
},
|
||||
} satisfies VersionedSelectorGroup;
|
||||
|
||||
export type VersionedPages = typeof versionedPages;
|
||||
|
||||
@@ -2,56 +2,34 @@ package gmsclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptoRand "crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
cryptoRand "crypto/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
)
|
||||
|
||||
// NewInMemoryClient returns an implementation of Client that returns canned responses
|
||||
func NewInMemoryClient() Client {
|
||||
return &memoryClientImpl{}
|
||||
return &memoryClientImpl{
|
||||
mx: &sync.Mutex{},
|
||||
snapshotInfo: make(map[string]cloudmigration.SnapshotState),
|
||||
}
|
||||
}
|
||||
|
||||
type memoryClientImpl struct {
|
||||
snapshot *cloudmigration.StartSnapshotResponse
|
||||
mx *sync.Mutex
|
||||
snapshotInfo map[string]cloudmigration.SnapshotState // snapshotUID -> state
|
||||
}
|
||||
|
||||
func (c *memoryClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.CloudMigrationSession) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memoryClientImpl) MigrateData(
|
||||
ctx context.Context,
|
||||
cm cloudmigration.CloudMigrationSession,
|
||||
request cloudmigration.MigrateDataRequest,
|
||||
) (*cloudmigration.MigrateDataResponse, error) {
|
||||
result := cloudmigration.MigrateDataResponse{
|
||||
Items: make([]cloudmigration.CloudMigrationResource, len(request.Items)),
|
||||
}
|
||||
|
||||
for i, v := range request.Items {
|
||||
result.Items[i] = cloudmigration.CloudMigrationResource{
|
||||
Type: v.Type,
|
||||
RefID: v.RefID,
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
// simulate flakiness on one random item
|
||||
i := rand.Intn(len(result.Items))
|
||||
failedItem := result.Items[i]
|
||||
failedItem.Status, failedItem.Error = cloudmigration.ItemStatusError, "simulated random error"
|
||||
result.Items[i] = failedItem
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *memoryClientImpl) StartSnapshot(_ context.Context, sess cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error) {
|
||||
publicKey, _, err := box.GenerateKey(cryptoRand.Reader)
|
||||
if err != nil {
|
||||
@@ -74,40 +52,54 @@ func (c *memoryClientImpl) StartSnapshot(_ context.Context, sess cloudmigration.
|
||||
return nil, fmt.Errorf("marshalling metadata: %w", err)
|
||||
}
|
||||
|
||||
c.snapshot = &cloudmigration.StartSnapshotResponse{
|
||||
c.mx.Lock()
|
||||
c.snapshotInfo[snapshotUid] = cloudmigration.SnapshotStateInitialized
|
||||
c.mx.Unlock()
|
||||
|
||||
return &cloudmigration.StartSnapshotResponse{
|
||||
EncryptionKey: publicKey[:],
|
||||
SnapshotID: snapshotUid,
|
||||
MaxItemsPerPartition: 10,
|
||||
Algo: "nacl",
|
||||
Metadata: metadataBuffer,
|
||||
}
|
||||
|
||||
return c.snapshot, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *memoryClientImpl) GetSnapshotStatus(ctx context.Context, session cloudmigration.CloudMigrationSession, snapshot cloudmigration.CloudMigrationSnapshot, offset int) (*cloudmigration.GetSnapshotStatusResponse, error) {
|
||||
gmsResp := &cloudmigration.GetSnapshotStatusResponse{
|
||||
State: cloudmigration.SnapshotStateFinished,
|
||||
Results: []cloudmigration.CloudMigrationResource{
|
||||
{
|
||||
Type: cloudmigration.DashboardDataType,
|
||||
RefID: "dash1",
|
||||
c.mx.Lock()
|
||||
snapshotInfo := c.snapshotInfo[snapshot.UID]
|
||||
c.mx.Unlock()
|
||||
|
||||
resources := make([]cloudmigration.CloudMigrationResource, 0, len(snapshot.Resources))
|
||||
for _, resource := range snapshot.Resources {
|
||||
if snapshotInfo == cloudmigration.SnapshotStateFinished {
|
||||
resources = append(resources, cloudmigration.CloudMigrationResource{
|
||||
Type: resource.Type,
|
||||
RefID: resource.RefID,
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
},
|
||||
{
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: "ds1",
|
||||
Status: cloudmigration.ItemStatusError,
|
||||
Error: "fake error",
|
||||
},
|
||||
{
|
||||
Type: cloudmigration.FolderDataType,
|
||||
RefID: "folder1",
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
resources = append(resources, cloudmigration.CloudMigrationResource{
|
||||
Type: resource.Type,
|
||||
RefID: resource.RefID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
gmsResp := &cloudmigration.GetSnapshotStatusResponse{
|
||||
State: snapshotInfo,
|
||||
Results: resources,
|
||||
}
|
||||
|
||||
c.mx.Lock()
|
||||
// Next call, transition to the next state.
|
||||
if c.snapshotInfo[snapshot.UID] == cloudmigration.SnapshotStateInitialized {
|
||||
c.snapshotInfo[snapshot.UID] = cloudmigration.SnapshotStateProcessing
|
||||
} else {
|
||||
c.snapshotInfo[snapshot.UID] = cloudmigration.SnapshotStateFinished
|
||||
}
|
||||
c.mx.Unlock()
|
||||
|
||||
return gmsResp, nil
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ export const CallToAction = () => {
|
||||
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
|
||||
</Text>
|
||||
|
||||
<Button disabled={createMigrationResponse.isLoading} onClick={() => setModalOpen(true)}>
|
||||
<Button
|
||||
data-testid="migrate-to-cloud-connect-session-modal-button"
|
||||
disabled={createMigrationResponse.isLoading}
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.cta.button">Migrate this instance to Cloud</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
+6
-1
@@ -167,6 +167,7 @@ export const ConnectModal = ({ isOpen, isLoading, error, hideModal, onConfirm }:
|
||||
})}
|
||||
id={tokenId}
|
||||
placeholder={t('migrate-to-cloud.connect-modal.body-token-field-placeholder', 'Paste token here')}
|
||||
data-testid="migrate-to-cloud-connect-session-modal-token-input"
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
@@ -176,7 +177,11 @@ export const ConnectModal = ({ isOpen, isLoading, error, hideModal, onConfirm }:
|
||||
<Button variant="secondary" onClick={hideModal}>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading || !token}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || !token}
|
||||
data-testid="migrate-to-cloud-connect-session-modal-connect-button"
|
||||
>
|
||||
{isLoading
|
||||
? t('migrate-to-cloud.connect-modal.connecting', 'Connecting to this stack...')
|
||||
: t('migrate-to-cloud.connect-modal.connect', 'Connect to this stack')}
|
||||
|
||||
@@ -96,6 +96,7 @@ export function MigrationSummary(props: MigrationSummaryProps) {
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={disconnectIsLoading ? 'spinner' : undefined}
|
||||
data-testid="migrate-to-cloud-summary-disconnect-button"
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.disconnect">Disconnect</Trans>
|
||||
</Button>
|
||||
@@ -115,6 +116,7 @@ export function MigrationSummary(props: MigrationSummaryProps) {
|
||||
onClick={onBuildSnapshot}
|
||||
icon={buildSnapshotIsLoading ? 'spinner' : undefined}
|
||||
variant="secondary"
|
||||
data-testid="migrate-to-cloud-summary-rebuild-snapshot-button"
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.rebuild-snapshot">Rebuild snapshot</Trans>
|
||||
</Button>
|
||||
@@ -125,6 +127,7 @@ export function MigrationSummary(props: MigrationSummaryProps) {
|
||||
disabled={isBusy || uploadSnapshotIsLoading}
|
||||
onClick={onUploadSnapshot}
|
||||
icon={uploadSnapshotIsLoading ? 'spinner' : undefined}
|
||||
data-testid="migrate-to-cloud-summary-upload-snapshot-button"
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.upload-migration">Upload snapshot</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -31,7 +31,12 @@ export function BuildSnapshotCTA(props: SnapshotCTAProps) {
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Button disabled={disabled} onClick={onClick} icon={isLoading ? 'spinner' : undefined}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
icon={isLoading ? 'spinner' : undefined}
|
||||
data-testid="migrate-to-cloud-configure-snapshot-build-snapshot-button"
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.summary.start-migration">Build snapshot</Trans>
|
||||
</Button>
|
||||
</CTAInfo>
|
||||
|
||||
@@ -22,3 +22,6 @@ max_open_conn = 2
|
||||
[smtp]
|
||||
enabled = true
|
||||
host = localhost:7777
|
||||
|
||||
[cloud_migration]
|
||||
developer_mode = true ; Enable developer mode to use in-memory implementations of 3rdparty services needed.
|
||||
|
||||
Reference in New Issue
Block a user