diff --git a/extensions/sql-migration/images/completingCutover.svg b/extensions/sql-migration/images/completingCutover.svg
new file mode 100644
index 0000000000..9a6e44f584
--- /dev/null
+++ b/extensions/sql-migration/images/completingCutover.svg
@@ -0,0 +1,7 @@
+
diff --git a/extensions/sql-migration/images/error.svg b/extensions/sql-migration/images/error.svg
new file mode 100644
index 0000000000..6eba0b225a
--- /dev/null
+++ b/extensions/sql-migration/images/error.svg
@@ -0,0 +1,11 @@
+
diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json
index d9e5414a8c..92c5d83f54 100644
--- a/extensions/sql-migration/package.json
+++ b/extensions/sql-migration/package.json
@@ -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",
diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts
index ef34bbc74a..3d7ce1f28d 100644
--- a/extensions/sql-migration/src/api/azure.ts
+++ b/extensions/sql-migration/src/api/azure.ts
@@ -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;
diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts
index 058605a5eb..0cd2a2b378 100644
--- a/extensions/sql-migration/src/api/utils.ts
+++ b/extensions/sql-migration/src/api/utils.ts
@@ -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(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;
+}
diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts
index fc348b100a..c0c62a58e3 100644
--- a/extensions/sql-migration/src/constants/iconPathHelper.ts
+++ b/extensions/sql-migration/src/constants/iconPathHelper.ts
@@ -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')
+ };
}
}
diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts
index 1ed2bafb86..1de7080822 100644
--- a/extensions/sql-migration/src/constants/strings.ts
+++ b/extensions/sql-migration/src/constants/strings.ts
@@ -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");
diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
index 8be02d8868..7d5af19b5a 100644
--- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
+++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
@@ -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 = 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: {
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
index 879ffa8acc..88ab74c262 100644
--- a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
@@ -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',
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
index 918a432778..391385251d 100644
--- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
@@ -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;
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
index 55a92916a5..b90960ae12 100644
--- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
@@ -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 = (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!,
- (this._statusDropdown.value).name
- );
+ const migrations = filterMigrations(this._model._migrations, (this._statusDropdown.value).name, this._searchBox.value!);
const data: azdata.DeclarativeTableCellValue[][] = [];
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
index 6b6c9d59af..56542fde03 100644
--- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
+++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
@@ -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'
}
diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts
index c98431f048..29542ed20a 100644
--- a/extensions/sql-migration/src/models/stateMachine.ts
+++ b/extensions/sql-migration/src/models/stateMachine.ts
@@ -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,
diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts
index 9685c9e69b..45a7b43b9e 100644
--- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts
+++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts
@@ -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 {
+
if (this.migrationStateModel.refreshDatabaseBackupPage) {
- this._targetDatabaseNames = [];
+ const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
+ const queryProvider = azdata.dataprotocol.getProvider((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({});
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 ((this._blobContainerResourceGroup.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
- errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
- }
- if ((this._blobContainerStorageAccountDropdown.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
- errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
- }
- if ((this._blobContainerDropdown.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) {
- errors.push(constants.INVALID_BLOBCONTAINER_ERROR);
+ this._blobContainerResourceGroupDropdowns.forEach(v => {
+ if ((v.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
+ errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
+ }
+ });
+ this._blobContainerStorageAccountDropdowns.forEach(v => {
+ if ((v.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
+ errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
+ }
+ });
+ this._blobContainerDropdowns.forEach(v => {
+ if ((v.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) {
+ errors.push(constants.INVALID_BLOBCONTAINER_ERROR);
+ }
+ });
+
+ const duplicates: Map = 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 {
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 {
}
- 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 {
+ 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 {
@@ -823,36 +973,37 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
private async loadblobResourceGroup(): Promise {
- 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 {
- this._blobContainerStorageAccountDropdown.loading = true;
+ private async loadblobStorageDropdown(index: number): Promise {
+ 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 {
- this._blobContainerDropdown.loading = true;
+ private async loadBlobContainerDropdown(index: number): Promise {
+ 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;
}
}
diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts
index 787d102a86..45cffa0156 100644
--- a/extensions/sql-migration/src/wizard/summaryPage.ts
+++ b/extensions/sql-migration/src/wizard/summaryPage.ts
@@ -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;
}
diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts
index 514e50f439..a0dff13bb8 100644
--- a/extensions/sql-migration/src/wizard/wizardController.ts
+++ b/extensions/sql-migration/src/wizard/wizardController.ts
@@ -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,