Migraiton enhancements v: 0.1.1 (#15570)

* adding filters and cards for failed migrations

* Added card and filter for completing cutover

* Fixing blob container support and some ux enhancements

* Enabling eastus2 and canada central regions

* Increasing height of container to accomodate newer cards and cleaning up database backup page

* vbump migration

* Removing unused code
This commit is contained in:
Aasim Khan
2021-05-25 18:00:30 -07:00
committed by GitHub
parent 730367494b
commit e49dd12951
16 changed files with 561 additions and 309 deletions

View File

@@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24Z" fill="#015CDA"/>
<path d="M7.5 12C7.50098 11.6796 7.53669 11.3602 7.6065 11.0475L4.6965 10.35C4.56899 10.8908 4.50307 11.4443 4.5 12C4.50776 12.6186 4.59241 13.2338 4.752 13.8315L7.6635 13.1355C7.56019 12.7656 7.50523 12.3839 7.5 12Z" fill="white"/>
<path d="M8.53486 14.8365L6.19336 16.689C6.91792 17.5931 7.84208 18.3171 8.89336 18.804L10.1519 16.0905C9.52399 15.8018 8.97079 15.3728 8.53486 14.8365Z" fill="white"/>
<path d="M8.4372 9.28199C8.86985 8.71437 9.4313 8.25781 10.0752 7.94999L8.8167 5.23499C7.74951 5.74195 6.81719 6.49387 6.0957 7.42949L8.4372 9.28199Z" fill="white"/>
<path d="M12 4.5V7.5C13.1935 7.5 14.3381 7.97411 15.182 8.81802C16.0259 9.66193 16.5 10.8065 16.5 12C16.5 13.1935 16.0259 14.3381 15.182 15.182C14.3381 16.0259 13.1935 16.5 12 16.5V19.5C13.9891 19.5 15.8968 18.7098 17.3033 17.3033C18.7098 15.8968 19.5 13.9891 19.5 12C19.5 10.0109 18.7098 8.10322 17.3033 6.6967C15.8968 5.29018 13.9891 4.5 12 4.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M12 24C14.3734 24 16.6935 23.2962 18.6668 21.9776C20.6402 20.6591 22.1783 18.7849 23.0866 16.5922C23.9948 14.3995 24.2324 11.9867 23.7694 9.65892C23.3064 7.33115 22.1635 5.19295 20.4853 3.51472C18.8071 1.83649 16.6689 0.693605 14.3411 0.230582C12.0133 -0.232441 9.60051 0.00519937 7.4078 0.913451C5.21509 1.8217 3.34094 3.35977 2.02236 5.33316C0.703788 7.30655 0 9.62663 0 12C0 15.1826 1.26428 18.2349 3.51472 20.4853C5.76516 22.7357 8.8174 24 12 24Z" fill="#E00B1C"/>
<path d="M18.8906 6.94196L16.7576 4.80896L11.8496 9.71696L6.94159 4.80896L4.80859 6.94196L9.71659 11.85L4.80859 16.758L6.94159 18.891L11.8496 13.983L16.7576 18.891L18.8906 16.758L13.9826 11.85L18.8906 6.94196Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 933 B

View File

@@ -2,7 +2,7 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.1.0",
"version": "0.1.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",

View File

@@ -32,7 +32,12 @@ export async function getLocations(account: azdata.Account, subscription: Subscr
throw new Error(response.errors.toString());
}
sortResourceArrayByName(response.locations);
const supportedLocations = ['eastus2', 'eastus2euap'];
const supportedLocations = [
'eastus2',
'eastus2euap',
'eastus',
'canadacentral'
];
const filteredLocations = response.locations.filter(loc => {
return supportedLocations.includes(loc.name);
});
@@ -377,8 +382,8 @@ export interface DatabaseMigration {
}
export interface DatabaseMigrationProperties {
scope: string;
provisioningState: string;
migrationStatus: string;
provisioningState: 'Succeeded' | 'Failed' | 'Creating';
migrationStatus: 'InProgress' | 'Failed' | 'Succeeded' | 'Creating' | 'Completing' | 'Cancelling';
migrationStatusDetails?: MigrationStatusDetails;
startedOn: string;
endedOn: string;

View File

@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { DAYS, HRS, MINUTE, SEC } from '../constants/strings';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { MigrationContext } from '../models/migrationLocalStorage';
export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') {
@@ -83,3 +85,38 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date):
return DAYS(parseFloat(days));
}
}
export function filterMigrations(databaseMigrations: MigrationContext[], statusFilter: string, databaseNameFilter?: string): MigrationContext[] {
let filteredMigration: MigrationContext[] = [];
if (statusFilter === AdsMigrationStatus.ALL) {
filteredMigration = databaseMigrations;
} else if (statusFilter === AdsMigrationStatus.ONGOING) {
filteredMigration = databaseMigrations.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
const provisioning = value.migrationContext.properties.provisioningState;
return status === 'InProgress' || status === 'Creating' || provisioning === 'Creating';
});
} else if (statusFilter === AdsMigrationStatus.SUCCEEDED) {
filteredMigration = databaseMigrations.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
return status === 'Succeeded';
});
} else if (statusFilter === AdsMigrationStatus.FAILED) {
filteredMigration = databaseMigrations.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
const provisioning = value.migrationContext.properties.provisioningState;
return status === 'Failed' || provisioning === 'Failed';
});
} else if (statusFilter === AdsMigrationStatus.COMPLETING) {
filteredMigration = databaseMigrations.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
return status === 'Completing';
});
}
if (databaseNameFilter) {
filteredMigration = filteredMigration.filter((value) => {
return value.migrationContext.name.toLowerCase().includes(databaseNameFilter.toLowerCase());
});
}
return filteredMigration;
}

View File

@@ -30,6 +30,8 @@ export class IconPathHelper {
public static cancel: IconPath;
public static warning: IconPath;
public static info: IconPath;
public static error: IconPath;
public static completingCutover: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -108,5 +110,13 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/info.svg'),
dark: context.asAbsolutePath('images/infoBox.svg')
};
IconPathHelper.error = {
light: context.asAbsolutePath('images/error.svg'),
dark: context.asAbsolutePath('images/error.svg')
};
IconPathHelper.completingCutover = {
light: context.asAbsolutePath('images/completingCutover.svg'),
dark: context.asAbsolutePath('images/completingCutover.svg')
};
}
}

View File

@@ -5,6 +5,7 @@
import { AzureAccount } from 'azurecore';
import * as nls from 'vscode-nls';
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
const localize = nls.loadMessageBundle();
@@ -144,6 +145,15 @@ export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.net
export const ENTER_BLOB_CONTAINER_INFORMATION = localize('sql.migration.blob.container.information', "Enter the target name and select the blob container location for selected databases");
export const ENTER_FILE_SHARE_INFORMATION = localize('sql.migration.enter.file.share.information', "Enter the target name and select the file share location of selected databases");
export const INVALID_TARGET_NAME_ERROR = localize('sql.migration.invalid.target.name.error', "Please enter a valid name for the target database.");
export const PROVIDE_UNIQUE_CONTAINERS = localize('sql.migration.provide.unique.containers', "Please provide unique containers for target databases. Databases affected: ");
export function SQL_SOURCE_DETAILS(authMethod: MigrationSourceAuthenticationType, serverName: string): string {
switch (authMethod) {
case MigrationSourceAuthenticationType.Integrated:
return localize('sql.migration.source.details.windowAuth', "Enter the Windows Authentication credential used for connecting to SQL Server Instance {0}. This credential will be used to for connecting to SQL Server instance and identifying valid backup file(s)", serverName);
case MigrationSourceAuthenticationType.Sql:
return localize('sql.migration.source.details.sqlAuth', "Enter the SQL Authentication credential used for connecting to SQL Server Instance {0}. This credential will be used to for connecting to SQL Server instance and identifying valid backup file(s)", serverName);
}
}
// integration runtime page
export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service");
@@ -261,8 +271,10 @@ export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account deta
export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine");
export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details");
export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migration in progress");
export const MIGRATION_FAILED = localize('sql.migration.failed', "Migration failed");
export const LOG_SHIPPING_IN_PROGRESS = localize('sql.migration.log.shipping.in.progress', "Log shipping in progress");
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migration completed");
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migration completed");
export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Completing cutover");
export const SUCCESSFULLY_MIGRATED_TO_AZURE_SQL = localize('sql.migration.successfully.migrated.to.azure.sql', "Successfully migrated to Azure SQL");
export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migration not started");
export const CHOOSE_TO_MIGRATE_TO_AZURE_SQL = localize('sql.migration.choose.to.migrate.to.azure.sql', "Choose to migrate to Azure SQL");

View File

@@ -9,7 +9,8 @@ import { MigrationContext, MigrationLocalStorage } from '../models/migrationLoca
import * as loc from '../constants/strings';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
import { MigrationCategory } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { filterMigrations } from '../api/utils';
interface IActionMetadata {
title?: string,
@@ -39,6 +40,8 @@ export class DashboardWidget {
private _inProgressMigrationButton!: StatusCard;
private _inProgressWarningMigrationButton!: StatusCard;
private _successfulMigrationButton!: StatusCard;
private _failedMigrationButton!: StatusCard;
private _completingMigrationButton!: StatusCard;
private _notStartedMigrationCard!: StatusCard;
private _migrationStatusMap: Map<string, MigrationContext[]> = new Map();
private _viewAllMigrationsButton!: azdata.ButtonComponent;
@@ -233,15 +236,9 @@ export class DashboardWidget {
this._migrationStatusCardLoadingContainer.loading = true;
try {
this.setCurrentMigrations(await this.getMigrations());
const migrationStatus = await this.getCurrentMigrations();
const inProgressMigrations = migrationStatus.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
const provisioning = value.migrationContext.properties.provisioningState;
return status === 'InProgress' || status === 'Creating' || status === 'Completing' || provisioning === 'Creating';
});
const migrations = await this.getCurrentMigrations();
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
let warningCount = 0;
for (let i = 0; i < inProgressMigrations.length; i++) {
if (
inProgressMigrations[i].asyncOperationResult?.error?.message ||
@@ -252,7 +249,6 @@ export class DashboardWidget {
warningCount += 1;
}
}
if (warningCount > 0) {
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
this._inProgressMigrationButton.container.display = 'none';
@@ -261,22 +257,32 @@ export class DashboardWidget {
this._inProgressMigrationButton.container.display = 'inline';
this._inProgressWarningMigrationButton.container.display = 'none';
}
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
const successfulMigration = migrationStatus.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
return status === 'Succeeded';
});
const successfulMigration = filterMigrations(migrations, AdsMigrationStatus.SUCCEEDED);
this._successfulMigrationButton.count.value = successfulMigration.length.toString();
const currentConnection = (await azdata.connection.getCurrentConnection());
const migrationDatabases = new Set(
migrationStatus.map((value) => {
return value.migrationContext.properties.sourceDatabaseName;
}));
const serverDatabases = await azdata.connection.listDatabases(currentConnection.connectionId);
this._notStartedMigrationCard.count.value = (serverDatabases.length - migrationDatabases.size).toString();
const failedMigrations = filterMigrations(migrations, AdsMigrationStatus.FAILED);
const failedCount = failedMigrations.length;
if (failedCount > 0) {
this._failedMigrationButton.container.display = 'inline';
this._failedMigrationButton.count.value = failedMigrations.length.toString();
} else {
this._failedMigrationButton.container.display = 'none';
}
const completingCutoverMigrations = filterMigrations(migrations, AdsMigrationStatus.COMPLETING);
const cutoverCount = completingCutoverMigrations.length;
if (cutoverCount > 0) {
this._completingMigrationButton.container.display = 'inline';
this._completingMigrationButton.count.value = cutoverCount.toString();
} else {
this._completingMigrationButton.container.display = 'none';
}
} catch (error) {
console.log(error);
} finally {
@@ -498,7 +504,7 @@ export class DashboardWidget {
const statusContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '400px',
height: '280px',
height: '350px',
justifyContent: 'flex-start',
}).withProps({
CSSStyles: {
@@ -527,7 +533,7 @@ export class DashboardWidget {
this._viewAllMigrationsButton.onDidClick(async (e) => {
const migrationStatus = await this.getCurrentMigrations();
new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), MigrationCategory.ALL).initialize();
new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize();
});
const refreshButton = view.modelBuilder.hyperlink().withProps({
@@ -581,7 +587,7 @@ export class DashboardWidget {
loc.MIGRATION_IN_PROGRESS
);
this._inProgressMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING);
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
dialog.initialize();
});
@@ -595,7 +601,7 @@ export class DashboardWidget {
''
);
this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING);
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
dialog.initialize();
});
@@ -608,13 +614,38 @@ export class DashboardWidget {
loc.MIGRATION_COMPLETED
);
this._successfulMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.SUCCEEDED);
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED);
dialog.initialize();
});
this._migrationStatusCardsContainer.addItem(
this._successfulMigrationButton.container
);
this._completingMigrationButton = this.createStatusCard(
IconPathHelper.completingCutover,
loc.MIGRATION_CUTOVER_CARD
);
this._completingMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING);
dialog.initialize();
});
this._migrationStatusCardsContainer.addItem(
this._completingMigrationButton.container
);
this._failedMigrationButton = this.createStatusCard(
IconPathHelper.error,
loc.MIGRATION_FAILED
);
this._failedMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.FAILED);
dialog.initialize();
});
this._migrationStatusCardsContainer.addItem(
this._failedMigrationButton.container
);
this._notStartedMigrationCard = this.createStatusCard(
IconPathHelper.notStartedMigration,
loc.MIGRATION_NOT_STARTED
@@ -650,7 +681,7 @@ export class DashboardWidget {
const linksContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '400px',
height: '280px',
height: '350px',
justifyContent: 'flex-start',
}).withProps({
CSSStyles: {

View File

@@ -72,7 +72,8 @@ export class ConfirmCutoverDialog {
}
}).component();
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length;
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length ?? 0;
const pendingText = this._view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '13px',

View File

@@ -517,9 +517,14 @@ export class MigrationCutoverDialog {
this._fullBackupFile.value = fullBackupFileName! ?? '-';
let backupLocation;
const isBlobMigration = this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined;
// Displaying storage accounts and blob container for azure blob backups.
if (this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob) {
backupLocation = `${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation.azureBlob.storageAccountResourceId.split('/').pop()} - ${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation.azureBlob.blobContainerName}`;
if (isBlobMigration) {
backupLocation = `${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.storageAccountResourceId.split('/').pop()} - ${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.blobContainerName}`;
this._fileCount.display = 'none';
this.fileTable.updateCssStyles({
'display': 'none'
});
} else {
backupLocation = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare?.path! ?? '-';
}
@@ -547,7 +552,7 @@ export class MigrationCutoverDialog {
if (migrationStatusTextValue === MigrationStatus.InProgress) {
const restoredCount = (this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(a => a.listOfBackupFiles[0].status === 'Restored'))?.length ?? 0;
if (restoredCount > 0) {
if (restoredCount > 0 || isBlobMigration) {
this._cutoverButton.enabled = true;
}
this._cancelButton.enabled = true;

View File

@@ -8,9 +8,9 @@ import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { MigrationContext, MigrationLocalStorage } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import * as loc from '../../constants/strings';
import { convertTimeDifferenceToDuration } from '../../api/utils';
import { convertTimeDifferenceToDuration, filterMigrations } from '../../api/utils';
export class MigrationStatusDialog {
private _model: MigrationStatusDialogModel;
private _dialogObject!: azdata.window.Dialog;
@@ -21,7 +21,7 @@ export class MigrationStatusDialog {
private _statusTable!: azdata.DeclarativeTableComponent;
private _refreshLoader!: azdata.LoadingComponent;
constructor(migrations: MigrationContext[], private _filter: MigrationCategory) {
constructor(migrations: MigrationContext[], private _filter: AdsMigrationStatus) {
this._model = new MigrationStatusDialogModel(migrations);
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide');
}
@@ -40,7 +40,11 @@ export class MigrationStatusDialog {
this.populateMigrationTable();
});
this._statusDropdown.value = this._statusDropdown.values![this._filter];
if (this._filter) {
this._statusDropdown.value = (<azdata.CategoryValue[]>this._statusDropdown.values).find((value) => {
return value.name === this._filter;
});
}
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
@@ -124,10 +128,7 @@ export class MigrationStatusDialog {
private populateMigrationTable(): void {
try {
const migrations = this._model.filterMigration(
this._searchBox.value!,
(<azdata.CategoryValue>this._statusDropdown.value).name
);
const migrations = filterMigrations(this._model._migrations, (<azdata.CategoryValue>this._statusDropdown.value).name, this._searchBox.value!);
const data: azdata.DeclarativeTableCellValue[][] = [];

View File

@@ -10,47 +10,34 @@ export class MigrationStatusDialogModel {
public statusDropdownValues: azdata.CategoryValue[] = [
{
displayName: 'Status: All',
name: 'All',
name: AdsMigrationStatus.ALL,
}, {
displayName: 'Status: Ongoing',
name: 'Ongoing',
name: AdsMigrationStatus.ONGOING,
}, {
displayName: 'Status: Completing',
name: AdsMigrationStatus.COMPLETING
}, {
displayName: 'Status: Succeeded',
name: 'Succeeded',
name: AdsMigrationStatus.SUCCEEDED,
}, {
displayName: 'Status: Failed',
name: AdsMigrationStatus.FAILED
}
];
constructor(public _migrations: MigrationContext[]) {
}
public filterMigration(databaseName: string, category: string): MigrationContext[] {
let filteredMigration: MigrationContext[] = [];
if (category === 'All') {
filteredMigration = this._migrations;
} else if (category === 'Ongoing') {
filteredMigration = this._migrations.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
const provisioning = value.migrationContext.properties.provisioningState;
return status === 'InProgress' || status === 'Creating' || status === 'Completing' || provisioning === 'Creating';
});
} else if (category === 'Succeeded') {
filteredMigration = this._migrations.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
return status === 'Succeeded';
});
}
if (databaseName) {
filteredMigration = filteredMigration.filter((value) => {
return value.migrationContext.name.toLowerCase().includes(databaseName.toLowerCase());
});
}
return filteredMigration;
}
}
export enum MigrationCategory {
ALL,
ONGOING,
SUCCEEDED
/**
* This enum is used to categorize migrations internally in ADS. A migration has 2 statuses: Provisioning Status and Migration Status. The values from both the statuses are mapped to different values in this enum
*/
export enum AdsMigrationStatus {
ALL = 'all',
ONGOING = 'ongoing',
SUCCEEDED = 'succeeded',
FAILED = 'failed',
COMPLETING = 'completing'
}

View File

@@ -58,10 +58,9 @@ export enum NetworkContainerType {
export interface DatabaseBackupModel {
migrationMode: MigrationMode;
networkContainerType: NetworkContainerType;
storageKey: string;
networkShare: NetworkShare;
subscription: azureResource.AzureResourceSubscription;
blob: Blob;
blobs: Blob[];
}
export interface NetworkShare {
@@ -70,12 +69,14 @@ export interface NetworkShare {
password: string;
resourceGroup: azureResource.AzureResourceResourceGroup;
storageAccount: StorageAccount;
storageKey: string;
}
export interface Blob {
resourceGroup: azureResource.AzureResourceResourceGroup;
storageAccount: StorageAccount;
blobContainer: azureResource.BlobContainer;
storageKey: string;
}
export interface Model {
@@ -144,7 +145,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._currentState = State.INIT;
this._databaseBackup = {} as DatabaseBackupModel;
this._databaseBackup.networkShare = {} as NetworkShare;
this._databaseBackup.blob = {} as Blob;
this._databaseBackup.blobs = [];
}
public get sourceConnectionId(): string {
@@ -667,38 +668,38 @@ export class MigrationStateModel implements Model, vscode.Disposable {
scope: this._targetServerInstance.id
}
};
switch (this._databaseBackup.networkContainerType) {
case NetworkContainerType.BLOB_CONTAINER:
requestBody.properties.backupConfiguration = {
targetLocation: undefined!,
sourceLocation: {
azureBlob: {
storageAccountResourceId: this._databaseBackup.blob.storageAccount.id,
accountKey: this._databaseBackup.storageKey,
blobContainerName: this._databaseBackup.blob.blobContainer.name
}
}
};
break;
case NetworkContainerType.NETWORK_SHARE:
requestBody.properties.backupConfiguration = {
targetLocation: {
storageAccountResourceId: this._databaseBackup.networkShare.storageAccount.id,
accountKey: this._databaseBackup.storageKey,
},
sourceLocation: {
fileShare: {
path: this._databaseBackup.networkShare.networkShareLocation,
username: this._databaseBackup.networkShare.windowsUser,
password: this._databaseBackup.networkShare.password,
}
}
};
break;
}
for (let i = 0; i < this._migrationDbs.length; i++) {
try {
switch (this._databaseBackup.networkContainerType) {
case NetworkContainerType.BLOB_CONTAINER:
requestBody.properties.backupConfiguration = {
targetLocation: undefined!,
sourceLocation: {
azureBlob: {
storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id,
accountKey: this._databaseBackup.blobs[i].storageKey,
blobContainerName: this._databaseBackup.blobs[i].blobContainer.name
}
}
};
break;
case NetworkContainerType.NETWORK_SHARE:
requestBody.properties.backupConfiguration = {
targetLocation: {
storageAccountResourceId: this._databaseBackup.networkShare.storageAccount.id,
accountKey: this._databaseBackup.networkShare.storageKey,
},
sourceLocation: {
fileShare: {
path: this._databaseBackup.networkShare.networkShareLocation,
username: this._databaseBackup.networkShare.windowsUser,
password: this._databaseBackup.networkShare.password,
}
}
};
break;
}
requestBody.properties.sourceDatabaseName = this._migrationDbs[i];
const response = await startDatabaseMigration(
this._azureAccount,

View File

@@ -7,9 +7,8 @@ import * as azdata from 'azdata';
import { EOL } from 'os';
import { getStorageAccountAccessKeys } from '../api/azure';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { Blob, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
export class DatabaseBackupPage extends MigrationWizardPage {
@@ -19,17 +18,16 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private _windowsUserAccountText!: azdata.InputBoxComponent;
private _passwordText!: azdata.InputBoxComponent;
private _networkSharePath!: azdata.InputBoxComponent;
private _sourceHelpText!: azdata.TextComponent;
private _sqlSourceUsernameInput!: azdata.InputBoxComponent;
private _sqlSourcepassword!: azdata.InputBoxComponent;
private _blobContainer!: azdata.FlexContainer;
private _blobContainerSubscription!: azdata.InputBoxComponent;
private _blobContainerLocation!: azdata.InputBoxComponent;
private _blobContainerResourceGroup!: azdata.DropDownComponent;
private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent;
private _blobContainerDropdown!: azdata.DropDownComponent;
private _fileShareContainer!: azdata.FlexContainer;
private _fileShareSubscription!: azdata.InputBoxComponent;
private _fileShareStorageAccountDropdown!: azdata.DropDownComponent;
private _blobContainerResourceGroupDropdowns!: azdata.DropDownComponent[];
private _blobContainerStorageAccountDropdowns!: azdata.DropDownComponent[];
private _blobContainerDropdowns!: azdata.DropDownComponent[];
private _networkShareStorageAccountDetails!: azdata.FlexContainer;
private _networkShareContainerSubscription!: azdata.InputBoxComponent;
@@ -39,8 +37,12 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private _networkShareContainerStorageAccountRefreshButton!: azdata.ButtonComponent;
private _targetDatabaseContainer!: azdata.FlexContainer;
private _targetDatabaseNamesTable!: azdata.DeclarativeTableComponent;
private _targetDatabaseNames: azdata.InputBoxComponent[] = [];
private _newtworkShareTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent;
private _blobContainerTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent;
private _networkTableContainer!: azdata.FlexContainer;
private _blobTableContainer!: azdata.FlexContainer;
private _networkShareTargetDatabaseNames: azdata.InputBoxComponent[] = [];
private _blobContainerTargetDatabaseNames: azdata.InputBoxComponent[] = [];
private _existingDatabases: string[] = [];
@@ -107,7 +109,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
networkShareButton.onDidChangeCheckedState((e) => {
if (e) {
this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE);
this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE);
}
});
@@ -122,25 +124,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
blobContainerButton.onDidChangeCheckedState((e) => {
if (e) {
this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
}
});
const fileShareButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL,
enabled: false,
CSSStyles: {
'font-size': '13px'
}
}).component();
fileShareButton.onDidChangeCheckedState((e) => {
if (e) {
vscode.window.showInformationMessage('Feature coming soon');
networkShareButton.checked = true;
//this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE);
this.switchNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
}
});
@@ -148,8 +132,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
[
selectLocationText,
networkShareButton,
blobContainerButton,
fileShareButton
blobContainerButton
]
).withLayout({
flexFlow: 'column'
@@ -161,19 +144,69 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private createNetworkDetailsContainer(): azdata.FlexContainer {
this._networkShareContainer = this.createNetworkShareContainer();
this._blobContainer = this.createBlobContainer();
this._fileShareContainer = this.createFileShareContainer();
const networkContainer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
this._networkShareContainer,
this._blobContainer,
this._fileShareContainer
]).component();
return networkContainer;
}
private createNetworkShareContainer(): azdata.FlexContainer {
const sqlSourceHeader = this._view.modelBuilder.text().withProps({
value: constants.SOURCE_CREDENTIALS,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '14px',
'font-weight': 'bold'
}
}).component();
this._sourceHelpText = this._view.modelBuilder.text().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
}
}).component();
const usernameLable = this._view.modelBuilder.text().withProps({
value: constants.USERNAME,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._sqlSourceUsernameInput = this._view.modelBuilder.inputBox().withProps({
required: true,
enabled: false,
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._sqlSourceUsernameInput.onTextChanged(value => {
this.migrationStateModel._sqlServerUsername = value;
});
const sqlPasswordLabel = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold',
}
}).component();
this._sqlSourcepassword = this._view.modelBuilder.inputBox().withProps({
required: true,
inputType: 'password',
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._sqlSourcepassword.onTextChanged(value => {
this.migrationStateModel._sqlServerPassword = value;
});
const networkShareHeading = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT,
width: WIZARD_INPUT_COMPONENT_WIDTH,
@@ -282,8 +315,17 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this.migrationStateModel._databaseBackup.networkShare.password = value;
});
const flexContainer = this._view.modelBuilder.flexContainer().withItems(
[
sqlSourceHeader,
this._sourceHelpText,
usernameLable,
this._sqlSourceUsernameInput,
sqlPasswordLabel,
this._sqlSourcepassword,
networkShareHeading,
networkShareHelpText,
networkLocationInputBoxLabel,
@@ -303,39 +345,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return flexContainer;
}
private createFileShareContainer(): azdata.FlexContainer {
const subscriptionLabel = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL,
requiredIndicator: true,
}).component();
this._fileShareSubscription = this._view.modelBuilder.inputBox().withProps({
enabled: false
}).component();
const storageAccountLabel = this._view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL,
}).component();
this._fileShareStorageAccountDropdown = this._view.modelBuilder.dropDown().component();
const flexContainer = this._view.modelBuilder.flexContainer()
.withItems(
[
subscriptionLabel,
this._fileShareSubscription,
storageAccountLabel,
this._fileShareStorageAccountDropdown
]
).withLayout({
flexFlow: 'column'
}).withProps({
display: 'none'
}).component();
return flexContainer;
}
private createBlobContainer(): azdata.FlexContainer {
const subscriptionLabel = this._view.modelBuilder.text()
@@ -364,66 +373,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
enabled: false
}).component();
const resourceGroupLabel = this._view.modelBuilder.text()
.withProps({
value: constants.RESOURCE_GROUP,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold'
}
}).component();
this._blobContainerResourceGroup = this._view.modelBuilder.dropDown().withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._blobContainerResourceGroup.onValueChanged(e => {
if (e.selected && e.selected !== constants.RESOURCE_GROUP_NOT_FOUND) {
this.migrationStateModel._databaseBackup.blob.resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index);
}
this.loadblobStorageDropdown();
});
const storageAccountLabel = this._view.modelBuilder.text()
.withProps({
value: constants.STORAGE_ACCOUNT,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold'
}
}).component();
this._blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown()
.withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => {
if (value.selected && value.selected !== constants.NO_STORAGE_ACCOUNT_FOUND) {
this.migrationStateModel._databaseBackup.blob.storageAccount = this.migrationStateModel.getStorageAccount(value.index);
}
await this.loadBlobContainerDropdown();
});
const blobContainerLabel = this._view.modelBuilder.text()
.withProps({
value: constants.BLOB_CONTAINER,
CSSStyles: {
'font-size': '13px',
'font-weight': 'bold'
}
}).component();
this._blobContainerDropdown = this._view.modelBuilder.dropDown()
.withProps({
width: WIZARD_INPUT_COMPONENT_WIDTH
}).component();
this._blobContainerDropdown.onValueChanged(async (value) => {
if (value.selected && value.selected !== constants.NO_BLOBCONTAINERS_FOUND) {
this.migrationStateModel._databaseBackup.blob.blobContainer = this.migrationStateModel.getBlobContainer(value.index);
}
});
const flexContainer = this._view.modelBuilder.flexContainer()
.withItems(
[
@@ -431,12 +380,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._blobContainerSubscription,
locationLabel,
this._blobContainerLocation,
resourceGroupLabel,
this._blobContainerResourceGroup,
storageAccountLabel,
this._blobContainerStorageAccountDropdown,
blobContainerLabel,
this._blobContainerDropdown,
]
).withLayout({
flexFlow: 'column'
@@ -449,12 +392,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private createTargetDatabaseContainer(): azdata.FlexContainer {
const rowCssStyle: azdata.CssStyles = {
'border': 'none',
'font-size': '13px',
'border-bottom': '1px solid',
};
const headerCssStyles: azdata.CssStyles = {
'border': 'none',
'font-size': '13px',
@@ -462,8 +399,13 @@ export class DatabaseBackupPage extends MigrationWizardPage {
'text-align': 'left',
'border-bottom': '1px solid',
};
const rowCssStyle: azdata.CssStyles = {
'border': 'none',
'font-size': '13px',
'border-bottom': '1px solid',
};
this._targetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({
this._newtworkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({
columns: [
{
displayName: constants.SOURCE_DATABASE,
@@ -483,11 +425,64 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
]
}).component();
this._blobContainerTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({
columns: [
{
displayName: constants.SOURCE_DATABASE,
valueType: azdata.DeclarativeDataType.string,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
isReadOnly: true,
width: '200px'
},
{
displayName: constants.TARGET_DATABASE_NAME,
valueType: azdata.DeclarativeDataType.component,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
isReadOnly: true,
width: '200px'
},
{
displayName: constants.RESOURCE_GROUP,
valueType: azdata.DeclarativeDataType.component,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
isReadOnly: true,
width: '200px'
},
{
displayName: constants.STORAGE_ACCOUNT,
valueType: azdata.DeclarativeDataType.component,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
isReadOnly: true,
width: '200px'
},
{
displayName: constants.BLOB_CONTAINER,
valueType: azdata.DeclarativeDataType.component,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
isReadOnly: true,
width: '200px'
}
]
}).component();
this._networkTableContainer = this._view.modelBuilder.flexContainer().withItems([
this._newtworkShareTargetDatabaseNamesTable
]).component();
this._blobTableContainer = this._view.modelBuilder.flexContainer().withItems([
this._blobContainerTargetDatabaseNamesTable
]).component();
const container = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
this._targetDatabaseNamesTable
this._networkTableContainer,
this._blobTableContainer
]).withProps({
display: 'none'
}).component();
@@ -631,23 +626,39 @@ export class DatabaseBackupPage extends MigrationWizardPage {
public async onPageEnter(): Promise<void> {
if (this.migrationStateModel.refreshDatabaseBackupPage) {
this._targetDatabaseNames = [];
const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider);
const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query);
const username = results.rows[0][0].displayValue;
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!;
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName);
this._sqlSourceUsernameInput.value = username;
this._sqlSourcepassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password;
this._networkShareTargetDatabaseNames = [];
this._blobContainerTargetDatabaseNames = [];
this._blobContainerResourceGroupDropdowns = [];
this._blobContainerStorageAccountDropdowns = [];
this._blobContainerDropdowns = [];
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) {
this._existingDatabases = await this.migrationStateModel.getManagedDatabases();
}
this.migrationStateModel._targetDatabaseNames = [];
const tableRows: azdata.DeclarativeTableCellValue[][] = [];
this.migrationStateModel._databaseBackup.blobs = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
const targetRow: azdata.DeclarativeTableCellValue[] = [];
this.migrationStateModel._targetDatabaseNames.push('');
this.migrationStateModel._databaseBackup.blobs.push(<Blob>{});
const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({
required: true,
value: db,
width: '280px'
width: '200px'
}).withValidation(c => {
if (this._targetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values.
if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values.
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
return false;
}
@@ -664,18 +675,105 @@ export class DatabaseBackupPage extends MigrationWizardPage {
targetDatabaseInput.onTextChanged((value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
});
this._targetDatabaseNames.push(targetDatabaseInput);
this._networkShareTargetDatabaseNames.push(targetDatabaseInput);
const blobtargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({
required: true,
value: db,
width: '200px'
}).withValidation(c => {
if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values.
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
return false;
}
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
return false;
}
return true;
}).component();
blobtargetDatabaseInput.onTextChanged((value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
});
this._blobContainerTargetDatabaseNames.push(blobtargetDatabaseInput);
const blobContainerResourceDropdown = this._view.modelBuilder.dropDown().withProps({
width: '200px'
}).component();
blobContainerResourceDropdown.onValueChanged(e => {
if (e.selected && e.selected !== constants.RESOURCE_GROUP_NOT_FOUND) {
this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index);
}
this.loadblobStorageDropdown(index);
});
this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown);
const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown()
.withProps({
width: '200px'
}).component();
blobContainerStorageAccountDropdown.onValueChanged(async (value) => {
if (value.selected && value.selected !== constants.NO_STORAGE_ACCOUNT_FOUND) {
this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(value.index);
}
await this.loadBlobContainerDropdown(index);
});
this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown);
const blobContainerDropdown = this._view.modelBuilder.dropDown()
.withProps({
width: '200px'
}).component();
blobContainerDropdown.onValueChanged(async (value) => {
if (value.selected && value.selected !== constants.NO_BLOBCONTAINERS_FOUND) {
this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(value.index);
}
});
this._blobContainerDropdowns.push(blobContainerDropdown);
});
let data: azdata.DeclarativeTableCellValue[][] = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
const targetRow: azdata.DeclarativeTableCellValue[] = [];
targetRow.push({
value: db
});
targetRow.push({
value: targetDatabaseInput
value: this._networkShareTargetDatabaseNames[index]
});
tableRows.push(targetRow);
data.push(targetRow);
});
this._newtworkShareTargetDatabaseNamesTable.dataValues = data;
data = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
const targetRow: azdata.DeclarativeTableCellValue[] = [];
targetRow.push({
value: db
});
targetRow.push({
value: this._blobContainerTargetDatabaseNames[index]
});
targetRow.push({
value: this._blobContainerResourceGroupDropdowns[index]
});
targetRow.push({
value: this._blobContainerStorageAccountDropdowns[index]
});
targetRow.push({
value: this._blobContainerDropdowns[index]
});
data.push(targetRow);
});
this._blobContainerTargetDatabaseNamesTable.dataValues = data;
this._targetDatabaseNamesTable.dataValues = tableRows;
this.migrationStateModel.refreshDatabaseBackupPage = false;
}
await this.getSubscriptionValues();
@@ -696,15 +794,39 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
break;
case NetworkContainerType.BLOB_CONTAINER:
if ((<azdata.CategoryValue>this._blobContainerResourceGroup.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
if ((<azdata.CategoryValue>this._blobContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
if ((<azdata.CategoryValue>this._blobContainerDropdown.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) {
errors.push(constants.INVALID_BLOBCONTAINER_ERROR);
this._blobContainerResourceGroupDropdowns.forEach(v => {
if ((<azdata.CategoryValue>v.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
}
});
this._blobContainerStorageAccountDropdowns.forEach(v => {
if ((<azdata.CategoryValue>v.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
});
this._blobContainerDropdowns.forEach(v => {
if ((<azdata.CategoryValue>v.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) {
errors.push(constants.INVALID_BLOBCONTAINER_ERROR);
}
});
const duplicates: Map<string, number[]> = new Map();
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer.id;
if (duplicates.has(blobContainerId)) {
duplicates.get(blobContainerId)?.push(i);
} else {
duplicates.set(blobContainerId, [i]);
}
}
duplicates.forEach((d) => {
if (d.length > 1) {
const dupString = `${d.map(index => this.migrationStateModel._migrationDbs[index]).join(', ')}`;
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
}
});
break;
}
@@ -727,13 +849,25 @@ export class DatabaseBackupPage extends MigrationWizardPage {
public async onPageLeave(): Promise<void> {
try {
const storageAccount = (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) ?
this.migrationStateModel._databaseBackup.blob.storageAccount : this.migrationStateModel._databaseBackup.networkShare.storageAccount;
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
case NetworkContainerType.BLOB_CONTAINER:
for (let i = 0; i < this.migrationStateModel._databaseBackup.blobs.length; i++) {
const storageAccount = this.migrationStateModel._databaseBackup.blobs[i].storageAccount;
this.migrationStateModel._databaseBackup.blobs[i].storageKey = (await getStorageAccountAccessKeys(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
storageAccount)).keyName1;
}
break;
case NetworkContainerType.NETWORK_SHARE:
const storageAccount = this.migrationStateModel._databaseBackup.networkShare.storageAccount;
this.migrationStateModel._databaseBackup.storageKey = (await getStorageAccountAccessKeys(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
storageAccount)).keyName1;
this.migrationStateModel._databaseBackup.networkShare.storageKey = (await getStorageAccountAccessKeys(
this.migrationStateModel._azureAccount,
this.migrationStateModel._databaseBackup.subscription,
storageAccount)).keyName1;
break;
}
} finally {
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
@@ -744,7 +878,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private toggleNetworkContainerFields(containerType: NetworkContainerType): void {
private switchNetworkContainerFields(containerType: NetworkContainerType): void {
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
@@ -752,11 +886,18 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this.wizard.nextButton.enabled = true;
this.migrationStateModel._databaseBackup.networkContainerType = containerType;
this._fileShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.FILE_SHARE) ? 'inline' : 'none' });
this._blobContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none' });
this._networkShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' });
this._networkShareStorageAccountDetails.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' });
this._targetDatabaseContainer.updateCssStyles({ 'display': 'inline' });
this._networkTableContainer.display = (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none';
this._blobTableContainer.display = (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none';
//Preserving the database Names between the 2 tables.
this.migrationStateModel._targetDatabaseNames.forEach((v, index) => {
this._networkShareTargetDatabaseNames[index].value = v;
this._blobContainerTargetDatabaseNames[index].value = v;
});
this._windowsUserAccountText.updateProperties({
required: containerType === NetworkContainerType.NETWORK_SHARE
@@ -764,11 +905,19 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._passwordText.updateProperties({
required: containerType === NetworkContainerType.NETWORK_SHARE
});
this._sqlSourceUsernameInput.updateProperties({
required: containerType === NetworkContainerType.NETWORK_SHARE
});
this._sqlSourcepassword.updateProperties({
required: containerType === NetworkContainerType.NETWORK_SHARE
});
this.validateFields();
}
private async validateFields(): Promise<void> {
await this._sqlSourceUsernameInput.validate();
await this._sqlSourcepassword.validate();
await this._networkSharePath.validate();
await this._windowsUserAccountText.validate();
await this._passwordText.validate();
@@ -776,12 +925,13 @@ export class DatabaseBackupPage extends MigrationWizardPage {
await this._networkShareStorageAccountResourceGroupDropdown.validate();
await this._networkShareContainerStorageAccountDropdown.validate();
await this._blobContainerSubscription.validate();
await this._blobContainerResourceGroup.validate();
await this._blobContainerStorageAccountDropdown.validate();
await this._blobContainerDropdown.validate();
await this._targetDatabaseNames.forEach((inputBox) => {
inputBox.validate();
});
for (let i = 0; i < this._networkShareTargetDatabaseNames.length; i++) {
await this._networkShareTargetDatabaseNames[i].validate();
await this._blobContainerTargetDatabaseNames[i].validate();
await this._blobContainerResourceGroupDropdowns[i].validate();
await this._blobContainerStorageAccountDropdowns[i].validate();
await this._blobContainerDropdowns[i].validate();
}
}
private async getSubscriptionValues(): Promise<void> {
@@ -823,36 +973,37 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
private async loadblobResourceGroup(): Promise<void> {
this._blobContainerResourceGroup.loading = true;
this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true);
try {
this._blobContainerResourceGroup.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription);
const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription);
this._blobContainerResourceGroupDropdowns.forEach(v => v.values = resourceGroupValues);
} catch (error) {
console.log(error);
} finally {
this._blobContainerResourceGroup.loading = false;
this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = false);
}
}
private async loadblobStorageDropdown(): Promise<void> {
this._blobContainerStorageAccountDropdown.loading = true;
private async loadblobStorageDropdown(index: number): Promise<void> {
this._blobContainerStorageAccountDropdowns[index].loading = true;
try {
this._blobContainerStorageAccountDropdown.values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blob.resourceGroup);
this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].resourceGroup);
} catch (error) {
console.log(error);
} finally {
this._blobContainerStorageAccountDropdown.loading = false;
this._blobContainerStorageAccountDropdowns[index].loading = false;
}
}
private async loadBlobContainerDropdown(): Promise<void> {
this._blobContainerDropdown.loading = true;
private async loadBlobContainerDropdown(index: number): Promise<void> {
this._blobContainerDropdowns[index].loading = true;
try {
const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blob.storageAccount);
this._blobContainerDropdown.values = blobContainerValues;
const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount);
this._blobContainerDropdowns[index].values = blobContainerValues;
} catch (error) {
console.log(error);
} finally {
this._blobContainerDropdown.loading = false;
this._blobContainerDropdowns[index].loading = false;
}
}

View File

@@ -96,29 +96,25 @@ export class SummaryPage extends MigrationWizardPage {
]
);
break;
case NetworkContainerType.FILE_SHARE:
flexContainer.addItems(
[
createInformationRow(this._view, constants.TYPE, constants.FILE_SHARE),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name),
]
);
break;
case NetworkContainerType.BLOB_CONTAINER:
flexContainer.addItems(
[
createInformationRow(this._view, constants.TYPE, constants.BLOB_CONTAINER),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name),
createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.blob.storageAccount.location),
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.blob.storageAccount.resourceGroup!),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.blob.storageAccount.name),
createInformationRow(this._view, constants.BLOB_CONTAINER, this.migrationStateModel._databaseBackup.blob.blobContainer.name)
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name)
]
);
}
flexContainer.addItem(createHeadingTextComponent(this._view, constants.TARGET_NAME));
this.migrationStateModel._migrationDbs.forEach((db, index) => {
flexContainer.addItem(createInformationRow(this._view, db, this.migrationStateModel._targetDatabaseNames[index]));
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
flexContainer.addItems([
createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.blobs[index].storageAccount.location),
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.blobs[index].storageAccount.resourceGroup!),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.blobs[index].storageAccount.name),
createInformationRow(this._view, constants.BLOB_CONTAINER, this.migrationStateModel._databaseBackup.blobs[index].blobContainer.name)
]);
}
});
return flexContainer;
}

View File

@@ -15,7 +15,6 @@ import { AccountsSelectionPage } from './accountsSelectionPage';
import { IntergrationRuntimePage } from './integrationRuntimePage';
import { SummaryPage } from './summaryPage';
import { MigrationModePage } from './migrationModePage';
import { SqlSourceConfigurationPage } from './sqlSourceConfigurationPage';
export const WIZARD_INPUT_COMPONENT_WIDTH = '600px';
export class WizardController {
@@ -40,14 +39,12 @@ export class WizardController {
const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel);
const migrationModePage = new MigrationModePage(wizard, stateModel);
const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel);
const sourceConfigurationPage = new SqlSourceConfigurationPage(wizard, stateModel);
const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel);
const integrationRuntimePage = new IntergrationRuntimePage(wizard, stateModel);
const summaryPage = new SummaryPage(wizard, stateModel);
const pages: MigrationWizardPage[] = [
azureAccountsPage,
sourceConfigurationPage,
skuRecommendationPage,
migrationModePage,
databaseBackupPage,