diff --git a/extensions/sql-migration/images/cutover.svg b/extensions/sql-migration/images/cutover.svg
new file mode 100644
index 0000000000..1a361e628f
--- /dev/null
+++ b/extensions/sql-migration/images/cutover.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/sql-migration/images/inProgress.svg b/extensions/sql-migration/images/inProgress.svg
new file mode 100644
index 0000000000..6ce7a6f03c
--- /dev/null
+++ b/extensions/sql-migration/images/inProgress.svg
@@ -0,0 +1,7 @@
+
diff --git a/extensions/sql-migration/images/notStarted.svg b/extensions/sql-migration/images/notStarted.svg
new file mode 100644
index 0000000000..53d53b5821
--- /dev/null
+++ b/extensions/sql-migration/images/notStarted.svg
@@ -0,0 +1,5 @@
+
diff --git a/extensions/sql-migration/images/sqlMI.svg b/extensions/sql-migration/images/sqlMI.svg
new file mode 100644
index 0000000000..e68cd269e4
--- /dev/null
+++ b/extensions/sql-migration/images/sqlMI.svg
@@ -0,0 +1,23 @@
+
diff --git a/extensions/sql-migration/images/sqlMiImportHelpThumbnail.svg b/extensions/sql-migration/images/sqlMiImportHelpThumbnail.svg
deleted file mode 100644
index 94fbb8932b..0000000000
--- a/extensions/sql-migration/images/sqlMiImportHelpThumbnail.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/extensions/sql-migration/images/sqlMiVideoThumbnail.svg b/extensions/sql-migration/images/sqlMiVideoThumbnail.svg
new file mode 100644
index 0000000000..afa2c63237
--- /dev/null
+++ b/extensions/sql-migration/images/sqlMiVideoThumbnail.svg
@@ -0,0 +1,18 @@
+
diff --git a/extensions/sql-migration/images/sqlVmImportHelpThumbnail.svg b/extensions/sql-migration/images/sqlVmImportHelpThumbnail.svg
deleted file mode 100644
index 1f1f586e01..0000000000
--- a/extensions/sql-migration/images/sqlVmImportHelpThumbnail.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/extensions/sql-migration/images/sqlVmVideoThumbnail.svg b/extensions/sql-migration/images/sqlVmVideoThumbnail.svg
new file mode 100644
index 0000000000..a9e0ea180c
--- /dev/null
+++ b/extensions/sql-migration/images/sqlVmVideoThumbnail.svg
@@ -0,0 +1,17 @@
+
diff --git a/extensions/sql-migration/images/succeeded.svg b/extensions/sql-migration/images/succeeded.svg
new file mode 100644
index 0000000000..1f8df787fc
--- /dev/null
+++ b/extensions/sql-migration/images/succeeded.svg
@@ -0,0 +1 @@
+
diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json
index 7d9654830d..2bc0d4f8fa 100644
--- a/extensions/sql-migration/package.json
+++ b/extensions/sql-migration/package.json
@@ -14,7 +14,6 @@
"activationEvents": [
"onDashboardOpen",
"onCommand:sqlmigration.start",
- "onCommand:sqlmigration.testDialog",
"onCommand:sqlmigration.openNotebooks"
],
"main": "./out/main",
@@ -32,11 +31,6 @@
"title": "SQL Migration Start",
"category": "SQL Migration"
},
- {
- "command": "sqlmigration.testDialog",
- "title": "SQL Migration test dialog",
- "category": "SQL Migration"
- },
{
"command": "sqlmigration.openNotebooks",
"title": "%migration-notebook-command-title%",
@@ -60,8 +54,8 @@
"name": "",
"row": 0,
"col": 1,
- "rowspan": 5,
- "colspan": 5,
+ "rowspan": 2.5,
+ "colspan": 3.5,
"widget": {
"modelview": {
"id": "migration.dashboard"
diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts
index b5903e027a..0d78849598 100644
--- a/extensions/sql-migration/src/api/azure.ts
+++ b/extensions/sql-migration/src/api/azure.ts
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azurecore from 'azurecore';
import { azureResource } from 'azureResource';
+import * as loc from '../constants/strings';
async function getAzureCoreAPI(): Promise {
const api = (await vscode.extensions.getExtension(azurecore.extension.name)?.activate()) as azurecore.IExtension;
@@ -82,7 +83,7 @@ export async function getBlobContainers(account: azdata.Account, subscription: S
return blobContainers!;
}
-export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise {
+export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise {
const api = await getAzureCoreAPI();
const host = `https://${regionName}.management.azure.com`;
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`;
@@ -93,7 +94,7 @@ export async function getMigrationController(account: azdata.Account, subscripti
return response.response.data;
}
-export async function getMigrationControllers(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string): Promise {
+export async function getMigrationControllers(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string): Promise {
const api = await getAzureCoreAPI();
const host = `https://${regionName}.management.azure.com`;
const path = `/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/Controllers?api-version=2020-09-01-preview`;
@@ -101,10 +102,11 @@ export async function getMigrationControllers(account: azdata.Account, subscript
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
+ sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
}
-export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise {
+export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise {
const api = await getAzureCoreAPI();
const host = `https://${regionName}.management.azure.com`;
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`;
@@ -157,10 +159,10 @@ export async function getMigrationControllerMonitoringData(account: azdata.Accou
return response.response.data;
}
-export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, migrationControllerName: string, requestBody: StartDatabaseMigrationRequest): Promise {
+export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest): Promise {
const api = await getAzureCoreAPI();
const host = `https://${regionName}.management.azure.com`;
- const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/managedInstances/${managedInstance}/providers/Microsoft.DataMigration/databaseMigrations/${migrationControllerName}?api-version=2020-09-01-preview`;
+ const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/managedInstances/${managedInstance}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
@@ -171,7 +173,18 @@ export async function startDatabaseMigration(account: azdata.Account, subscripti
};
}
-export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise {
+export async function getDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, migrationId: string): Promise {
+ const api = await getAzureCoreAPI();
+ const host = `https://${regionName}.management.azure.com`;
+ const path = `${migrationId}?api-version=2020-09-01-preview`;
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
+ if (response.errors.length > 0) {
+ throw new Error(response.errors.toString());
+ }
+ return response.response.data;
+}
+
+export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise {
const api = await getAzureCoreAPI();
const host = `https://eastus2euap.management.azure.com`;
const path = `${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`;
@@ -179,9 +192,29 @@ export async function getMigrationStatus(account: azdata.Account, subscription:
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
- return {
- result: response.response.data
- };
+ return response.response.data;
+}
+
+export async function listMigrationsByController(account: azdata.Account, subscription: Subscription, controller: SqlMigrationController): Promise {
+ const api = await getAzureCoreAPI();
+ const host = `https://eastus2euap.management.azure.com`;
+ const path = `${controller.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`;
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
+ if (response.errors.length > 0) {
+ throw new Error(response.errors.toString());
+ }
+ return response.response.data.value;
+}
+
+export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration): Promise {
+ const api = await getAzureCoreAPI();
+ const host = `https://eastus2euap.management.azure.com`;
+ const path = `${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cutover?api-version=2020-09-01-preview`;
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host);
+ if (response.errors.length > 0) {
+ throw new Error(response.errors.toString());
+ }
+ return response.response.data.value;
}
/**
@@ -190,13 +223,13 @@ export async function getMigrationStatus(account: azdata.Account, subscription:
export function getMigrationControllerRegions(): azdata.CategoryValue[] {
return [
{
- displayName: 'East US EUAP',
+ displayName: loc.EASTUS2EUAP,
name: 'eastus2euap'
}
];
}
-type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription;
+type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription | SqlMigrationController;
function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void {
if (!resourceArray) {
return;
@@ -222,7 +255,7 @@ export interface MigrationControllerProperties {
isProvisioned?: boolean;
}
-export interface MigrationController {
+export interface SqlMigrationController {
properties: MigrationControllerProperties;
location: string;
id: string;
@@ -285,19 +318,105 @@ export interface StartDatabaseMigrationRequest {
}
}
-export interface DatabaseMigration {
- properties: {
- name: string,
- provisioningState: string,
- sourceDatabaseName: string,
- migrationOperationId: string,
- },
- id: string,
- name: string,
- type: string
-}
-
export interface StartDatabaseMigrationResponse {
status: number,
databaseMigration: DatabaseMigration
}
+
+export interface DatabaseMigration {
+ properties: DatabaseMigrationProperties;
+ id: string;
+ name: string;
+ type: string;
+}
+export interface DatabaseMigrationProperties {
+ scope: string;
+ provisioningState: string;
+ migrationStatus: string;
+ migrationStatusDetails?: MigrationStatusDetails;
+ sourceSqlConnection: SqlConnectionInfo;
+ sourceDatabaseName: string;
+ targetDatabaseCollation: string;
+ migrationController: string;
+ migrationOperationId: string;
+ backupConfiguration: BackupConfiguration;
+ autoCutoverConfiguration: AutoCutoverConfiguration;
+ migrationFailureError: ErrorInfo;
+}
+export interface MigrationStatusDetails {
+ migrationState: string;
+ startedOn: string;
+ endedOn: string;
+ fullBackupSetInfo: BackupSetInfo;
+ lastRestoredBackupSetInfo: BackupSetInfo;
+ activeBackupSets: BackupSetInfo[];
+ blobContainerName: string;
+ isFullBackupRestored: boolean;
+ restoreBlockingReason: string;
+ fileUploadBlockingErrors: string[];
+ currentRestoringFileName: string;
+ lastRestoredFilename: string;
+}
+
+export interface SqlConnectionInfo {
+ dataSource: string;
+ authentication: string;
+ username: string;
+ password: string;
+ encryptConnection: string;
+ trustServerCertificate: string;
+}
+
+export interface BackupConfiguration {
+ sourceLocation: SourceLocation;
+ targetLocation: TargetLocation;
+}
+
+export interface AutoCutoverConfiguration {
+ lastBackupName: string;
+}
+
+export interface ErrorInfo {
+ code: string;
+ message: string;
+}
+
+export interface BackupSetInfo {
+ backupSetId: string;
+ firstLSN: string;
+ lastLSN: string;
+ backupType: string;
+ listOfBackupFiles: BackupFileInfo[];
+ backupStartDate: string;
+ backupFinishDate: string;
+ isBackupRestored: boolean;
+ backupSize: number;
+ compressedBackupSize: number;
+}
+
+export interface SourceLocation {
+ fileShare: DatabaseMigrationFileShare;
+ azureBlob: DatabaseMigrationAzureBlob;
+}
+
+export interface TargetLocation {
+ storageAccountResourceId: string;
+ accountKey: string;
+}
+
+export interface BackupFileInfo {
+ fileName: string;
+ status: string;
+}
+
+export interface DatabaseMigrationFileShare {
+ path: string;
+ username: string;
+ password: string;
+}
+
+export interface DatabaseMigrationAzureBlob {
+ storageAccountResourceId: string;
+ accountKey: string;
+ blobContainerName: string;
+}
diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts
index 64c3eec683..15e9de31f7 100644
--- a/extensions/sql-migration/src/constants/iconPathHelper.ts
+++ b/extensions/sql-migration/src/constants/iconPathHelper.ts
@@ -13,10 +13,14 @@ export interface IconPath {
export class IconPathHelper {
public static copy: IconPath;
public static refresh: IconPath;
- public static sqlMiImportHelpThumbnail: IconPath;
- public static sqlVmImportHelpThumbnail: IconPath;
- public static migrationDashboardHeaderBackground: IconPath;
+ public static cutover: IconPath;
public static sqlMigrationLogo: IconPath;
+ public static sqlMiVideoThumbnail: IconPath;
+ public static sqlVmVideoThumbnail: IconPath;
+ public static migrationDashboardHeaderBackground: IconPath;
+ public static inProgressMigration: IconPath;
+ public static completedMigration: IconPath;
+ public static notStartedMigration: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -27,13 +31,13 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/refresh.svg'),
dark: context.asAbsolutePath('images/refresh.svg')
};
- IconPathHelper.sqlMiImportHelpThumbnail = {
- light: context.asAbsolutePath('images/sqlMiImportHelpThumbnail.svg'),
- dark: context.asAbsolutePath('images/sqlMiImportHelpThumbnail.svg')
+ IconPathHelper.sqlMiVideoThumbnail = {
+ light: context.asAbsolutePath('images/sqlMiVideoThumbnail.svg'),
+ dark: context.asAbsolutePath('images/sqlMiVideoThumbnail.svg')
};
- IconPathHelper.sqlVmImportHelpThumbnail = {
- light: context.asAbsolutePath('images/sqlVmImportHelpThumbnail.svg'),
- dark: context.asAbsolutePath('images/sqlVmImportHelpThumbnail.svg')
+ IconPathHelper.sqlVmVideoThumbnail = {
+ light: context.asAbsolutePath('images/sqlVmVideoThumbnail.svg'),
+ dark: context.asAbsolutePath('images/sqlVmVideoThumbnail.svg')
};
IconPathHelper.migrationDashboardHeaderBackground = {
light: context.asAbsolutePath('images/background.svg'),
@@ -43,5 +47,21 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/migration.svg'),
dark: context.asAbsolutePath('images/migration.svg')
};
+ IconPathHelper.inProgressMigration = {
+ light: context.asAbsolutePath('images/inProgress.svg'),
+ dark: context.asAbsolutePath('images/inProgress.svg')
+ };
+ IconPathHelper.completedMigration = {
+ light: context.asAbsolutePath('images/succeeded.svg'),
+ dark: context.asAbsolutePath('images/succeeded.svg')
+ };
+ IconPathHelper.notStartedMigration = {
+ light: context.asAbsolutePath('images/notStarted.svg'),
+ dark: context.asAbsolutePath('images/notStarted.svg')
+ };
+ IconPathHelper.cutover = {
+ light: context.asAbsolutePath('images/cutover.svg'),
+ dark: context.asAbsolutePath('images/cutover.svg')
+ };
}
}
diff --git a/extensions/sql-migration/src/constants/notebookPathHelper.ts b/extensions/sql-migration/src/constants/notebookPathHelper.ts
index 7b1e84da08..b901307c3a 100644
--- a/extensions/sql-migration/src/constants/notebookPathHelper.ts
+++ b/extensions/sql-migration/src/constants/notebookPathHelper.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
-import * as loc from '../models/strings';
+import * as loc from './strings';
export class NotebookPathHelper {
private static context: vscode.ExtensionContext;
diff --git a/extensions/sql-migration/src/models/strings.ts b/extensions/sql-migration/src/constants/strings.ts
similarity index 84%
rename from extensions/sql-migration/src/models/strings.ts
rename to extensions/sql-migration/src/constants/strings.ts
index ae5e6bf17d..6c885f7f35 100644
--- a/extensions/sql-migration/src/models/strings.ts
+++ b/extensions/sql-migration/src/constants/strings.ts
@@ -192,4 +192,52 @@ export const PRE_REQ_TITLE = localize('sql.migration.pre.req.title', "Things you
export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account details");
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', "Migration in progress");
+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', "Migration completed");
+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");
+
+// Azure APIs
+export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP');
+
+
+//Migration cutover dialog
+export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
+export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
+export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
+export const TARGET_SERVER = localize('sql.migration.target.server', "Target server");
+export const TARGET_VERSION = localize('sql.migration.target.version', "Target version");
+export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status");
+export const FULL_BACKUP_FILES = localize('sql.migration.full.backup.files', "Full backup files(s)");
+export const LAST_APPLIED_LSN = localize('sql.migration.last.applied.lsn', "Last applied LSN");
+export const LAST_APPLIED_BACKUP_FILES = localize('sql.migration.last.applied.backup.files', "Last applied backup file(s)");
+export const LAST_APPLIED_BACKUP_FILES_TAKEN_ON = localize('sql.migration.last.applied.files.taken.on', "Last applied backup file(s) taken on");
+export const ACTIVE_BACKUP_FILES = localize('sql.migration.active.backup.files', "Active Backup file(s)");
+export const STATUS = localize('sql.migration.status', "Status");
+export const BACKUP_START_TIME = localize('sql.migration.backup.start.time', "Backup start time");
+export const FIRST_LSN = localize('sql.migration.first.lsn', "First LSN");
+export const LAST_LSN = localize('sql.migration.last.LSN', "Last LSN");
+export const CANNOT_START_CUTOVER_ERROR = localize('sql.migration.cannot.start.cutover.error', "Cannot start the cutover process until all the migrations are done. Click refresh to fetch the latest file status");
+export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Database Managed Instance");
+export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "Azure SQL Database Virtual Machine");
+
+export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) {
+ if (fileCount === 1) {
+ return localize('sql.migration.active.backup.files.items', "Active Backup files (1 item)");
+ } else {
+ return localize('sql.migration.active.backup.files.multiple.items', "Active Backup files ({0} items)", fileCount);
+ }
+}
+
+
+//Migration status dialog
+export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
+export const ONLINE = localize('sql.migration.online', "Online");
+export const DATABASE = localize('sql.migration.database', "Database");
+export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target Azure SQL Instance Name");
+export const CUTOVER_TYPE = localize('sql.migration.cutover.type', "Cutover type");
+export const START_TIME = localize('sql.migration.start.time', "Start Time");
+export const FINISH_TIME = localize('sql.migration.finish.time', "Finish Time");
diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
index ce8e81e8c3..dd0c5c010f 100644
--- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
+++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
@@ -5,9 +5,12 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
-import { MigrationLocalStorage } from '../models/migrationLocalStorage';
-import * as loc from '../models/strings';
-import { IconPathHelper } from '../constants/iconPathHelper';
+import { MigrationContext, MigrationLocalStorage } from '../models/migrationLocalStorage';
+import * as loc from '../constants/strings';
+import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
+import { getDatabaseMigration } from '../api/azure';
+import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
+import { MigrationCategory } from '../dialog/migrationStatus/migrationStatusDialogModel';
interface IActionMetadata {
title?: string,
@@ -22,6 +25,7 @@ const maxWidth = 800;
export class DashboardWidget {
private _migrationStatusCardsContainer!: azdata.FlexContainer;
+ private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
private _view!: azdata.ModelView;
constructor() {
@@ -30,47 +34,36 @@ export class DashboardWidget {
public register(): void {
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
this._view = view;
+
const container = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
+
const header = this.createHeader(view);
-
- const tasksContainer = await this.createTasks(view);
-
container.addItem(header, {
CSSStyles: {
'background-image': `url(${vscode.Uri.file(IconPathHelper.migrationDashboardHeaderBackground.light)})`,
- 'width': '1100px',
- 'height': '300px',
+ 'width': '870px',
+ 'height': '260px',
'background-size': '100%',
}
});
+
+ const tasksContainer = await this.createTasks(view);
header.addItem(tasksContainer, {
CSSStyles: {
'width': `${maxWidth}px`,
'height': '150px',
}
});
-
- header.addItem(await this.createFooter(view), {
+ container.addItem(await this.createFooter(view), {
CSSStyles: {
'margin-top': '20px'
}
});
-
- const mainContainer = view.modelBuilder.flexContainer()
- .withLayout({
- flexFlow: 'column',
- width: '100%',
- height: '100%',
- position: 'absolute'
- }).component();
- mainContainer.addItem(container, {
- CSSStyles: { 'padding-top': '25px', 'padding-left': '5px' }
- });
- await view.initializeModel(mainContainer);
+ await view.initializeModel(container);
this.refreshMigrations();
});
@@ -85,12 +78,14 @@ export class DashboardWidget {
value: loc.DASHBOARD_TITLE,
CSSStyles: {
'font-size': '36px',
+ 'margin-bottom': '5px',
}
}).component();
const descComponent = view.modelBuilder.text().withProps({
value: loc.DASHBOARD_DESCRIPTION,
CSSStyles: {
'font-size': '12px',
+ 'margin-top': '10px',
}
}).component();
header.addItems([titleComponent, descComponent], {
@@ -99,7 +94,6 @@ export class DashboardWidget {
'padding-left': '20px'
}
});
-
return header;
}
@@ -135,7 +129,9 @@ export class DashboardWidget {
const preRequisiteListElement = view.modelBuilder.text().withProps({
value: points,
CSSStyles: {
- 'padding-left': '15px'
+ 'padding-left': '15px',
+ 'margin-bottom': '5px',
+ 'margin-top': '10px'
}
}).component();
@@ -160,7 +156,6 @@ export class DashboardWidget {
}
});
-
tasksContainer.addItem(migrateButton, {
CSSStyles: {
'margin-top': '20px',
@@ -199,19 +194,172 @@ export class DashboardWidget {
}
private async refreshMigrations(): Promise {
+ this._migrationStatusCardLoadingContainer.loading = true;
this._migrationStatusCardsContainer.clearItems();
- const currentConnection = (await azdata.connection.getCurrentConnection());
- const getMigrations = MigrationLocalStorage.getMigrations(currentConnection);
- getMigrations.forEach((migration) => {
- const button = this._view.modelBuilder.button().withProps({
- label: `Migration to ${migration.targetManagedInstance.name} using controller ${migration.migrationContext.name}`
- }).component();
- button.onDidClick(async (e) => {
+ try {
+ const migrationStatus = await this.getMigrations();
+
+ const inProgressMigrations = migrationStatus.filter((value) => {
+ const status = value.migrationContext.properties.migrationStatus;
+ return status === 'InProgress' || status === 'Creating' || status === 'Completing';
+ });
+ const inProgressMigrationButton = this.createStatusCard(
+ IconPathHelper.inProgressMigration,
+ loc.MIGRATION_IN_PROGRESS,
+ loc.LOG_SHIPPING_IN_PROGRESS,
+ inProgressMigrations.length
+ );
+ inProgressMigrationButton.onDidClick((e) => {
+ const dialog = new MigrationStatusDialog(migrationStatus, MigrationCategory.ONGOING);
+ dialog.initialize();
+ });
+ this._migrationStatusCardsContainer.addItem(inProgressMigrationButton);
+
+ const successfulMigration = migrationStatus.filter((value) => {
+ const status = value.migrationContext.properties.migrationStatus;
+ return status === 'Succeeded';
+ });
+ const successfulMigrationButton = this.createStatusCard(
+ IconPathHelper.completedMigration,
+ loc.MIGRATION_COMPLETED,
+ loc.SUCCESSFULLY_MIGRATED_TO_AZURE_SQL,
+ successfulMigration.length
+ );
+ successfulMigrationButton.onDidClick((e) => {
+ const dialog = new MigrationStatusDialog(migrationStatus, MigrationCategory.SUCCEEDED);
+ dialog.initialize();
});
this._migrationStatusCardsContainer.addItem(
- button
+ successfulMigrationButton
);
+
+ const currentConnection = (await azdata.connection.getCurrentConnection());
+ const localMigrations = MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection);
+ const migrationDatabases = new Set(
+ localMigrations.filter((value) => {
+
+ }).map((value) => {
+ return value.migrationContext.properties.sourceDatabaseName;
+ }));
+ const serverDatabases = await azdata.connection.listDatabases(currentConnection.connectionId);
+ const notStartedMigrationCard = this.createStatusCard(
+ IconPathHelper.notStartedMigration,
+ loc.MIGRATION_NOT_STARTED,
+ loc.CHOOSE_TO_MIGRATE_TO_AZURE_SQL,
+ serverDatabases.length - migrationDatabases.size
+ );
+ notStartedMigrationCard.onDidClick((e) => {
+ vscode.window.showInformationMessage('Feature coming soon');
+ });
+ this._migrationStatusCardsContainer.addItem(
+ notStartedMigrationCard
+ );
+ } catch (error) {
+ console.log(error);
+ } finally {
+ this._migrationStatusCardLoadingContainer.loading = false;
+ }
+
+ }
+
+ private async getMigrations(): Promise {
+ const currentConnection = (await azdata.connection.getCurrentConnection());
+ const localMigrations = MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection);
+ for (let i = 0; i < localMigrations.length; i++) {
+ const localMigration = localMigrations[i];
+ localMigration.migrationContext = await getDatabaseMigration(
+ localMigration.azureAccount,
+ localMigration.subscription,
+ localMigration.targetManagedInstance.location,
+ localMigration.migrationContext.id
+ );
+ localMigration.sourceConnectionProfile = currentConnection;
+ }
+ return localMigrations;
+ }
+
+ private createStatusCard(
+ cardIconPath: IconPath,
+ cardTitle: string,
+ cardDescription: string,
+ count: number
+ ): azdata.DivContainer {
+
+ const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({
+ CSSStyles: {
+ 'font-weight': 'bold',
+ 'height': '20px',
+ 'margin-top': '6px',
+ 'margin-bottom': '0px',
+ 'width': '300px',
+ 'font-size': '14px'
+ }
+ }).component();
+ const cardDescriptionText = this._view.modelBuilder.text().withProps({ value: cardDescription }).withProps({
+ CSSStyles: {
+ 'height': '18px',
+ 'margin-top': '0px',
+ 'margin-bottom': '0px',
+ 'width': '300px'
+ }
+ }).component();
+ const cardCount = this._view.modelBuilder.text().withProps({
+ value: count.toString(),
+ CSSStyles: {
+ 'font-size': '28px',
+ 'line-height': '36px',
+ 'margin-top': '4px'
+ }
+ }).component();
+
+ const flexContainer = this._view.modelBuilder.flexContainer().withItems([
+ cardTitleText,
+ cardDescriptionText
+ ]).withLayout({
+ flexFlow: 'column'
+ }).withProps({
+ CSSStyles: {
+ 'width': '300px',
+ 'height': '50px'
+ }
+ }).component();
+
+ const flex = this._view.modelBuilder.flexContainer().withProps({
+ CSSStyles: {
+ 'width': '400px',
+ 'height': '50px',
+ 'margin-top': '10px',
+ 'box-shadow': '0 1px 2px 0 rgba(0,0,0,0.2)'
+ }
+ }).component();
+
+ const img = this._view.modelBuilder.image().withProps({
+ iconPath: cardIconPath!.light,
+ iconHeight: 16,
+ iconWidth: 16,
+ width: 64,
+ height: 50
+ }).component();
+
+ flex.addItem(img, {
+ flex: '0'
});
+ flex.addItem(flexContainer, {
+ flex: '0',
+ CSSStyles: {
+ 'width': '300px'
+ }
+ });
+ flex.addItem(cardCount, {
+ flex: '0'
+ });
+
+ const compositeButton = this._view.modelBuilder.divContainer().withItems([flex]).withProps({
+ ariaRole: 'button',
+ ariaLabel: 'show status',
+ clickable: true
+ }).component();
+ return compositeButton;
}
private async createFooter(view: azdata.ModelView): Promise {
@@ -258,14 +406,22 @@ export class DashboardWidget {
const viewAllButton = view.modelBuilder.hyperlink().withProps({
label: loc.VIEW_ALL,
- url: ''
+ url: '',
+ CSSStyles: {
+ 'font-size': '13px'
+ }
}).component();
+ viewAllButton.onDidClick(async (e) => {
+ new MigrationStatusDialog(await this.getMigrations(), MigrationCategory.ALL).initialize();
+ });
+
const refreshButton = view.modelBuilder.hyperlink().withProps({
label: loc.REFRESH,
url: '',
CSSStyles: {
- 'text-align': 'right'
+ 'text-align': 'right',
+ 'font-size': '13px'
}
}).component();
@@ -305,6 +461,7 @@ export class DashboardWidget {
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
+ this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent().withItem(this._migrationStatusCardsContainer).component();
statusContainer.addItem(
header, {
@@ -318,7 +475,7 @@ export class DashboardWidget {
}
);
- statusContainer.addItem(this._migrationStatusCardsContainer, {
+ statusContainer.addItem(this._migrationStatusCardLoadingContainer, {
CSSStyles: {
'margin-top': '30px'
}
@@ -374,12 +531,12 @@ export class DashboardWidget {
const videosContainer = this.createVideoLinkContainers(view, [
{
- iconPath: IconPathHelper.sqlMiImportHelpThumbnail,
+ iconPath: IconPathHelper.sqlMiVideoThumbnail,
description: loc.HELP_VIDEO1_TITLE,
link: 'https://www.youtube.com/watch?v=sE99cSoFOHs' //TODO: Fix Video link
},
{
- iconPath: IconPathHelper.sqlVmImportHelpThumbnail,
+ iconPath: IconPathHelper.sqlVmVideoThumbnail,
description: loc.HELP_VIDEO2_TITLE,
link: 'https://www.youtube.com/watch?v=R4GCBoxADyQ' //TODO: Fix video link
}
@@ -447,13 +604,10 @@ export class DashboardWidget {
flexFlow: 'row',
width: maxWidth,
}).component();
-
links.forEach(link => {
const videoContainer = this.createVideoLink(view, link);
-
videosContainer.addItem(videoContainer);
});
-
return videosContainer;
}
diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts
index eb08fc1f21..642e8b5f55 100644
--- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts
+++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts
@@ -7,6 +7,7 @@ import * as azdata from 'azdata';
import { MigrationStateModel } from '../../models/stateMachine';
import { SqlDatabaseTree } from './sqlDatabasesTree';
import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
+import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
export type Issues = {
description: string,
@@ -30,7 +31,7 @@ export class AssessmentResultsDialog {
private _tree: SqlDatabaseTree;
- constructor(public ownerUri: string, public model: MigrationStateModel, public title: string) {
+ constructor(public ownerUri: string, public model: MigrationStateModel, public title: string, private skuRecommendationPage: SKURecommendationPage) {
this._model = model;
let assessmentData = this.parseData(this._model);
this._tree = new SqlDatabaseTree(this._model, assessmentData);
@@ -126,6 +127,7 @@ export class AssessmentResultsDialog {
protected async execute() {
this.model._migrationDbs = this._tree.selectedDbs();
+ this.skuRecommendationPage.refreshDatabaseCount(this._model._migrationDbs.length);
this._isOpen = false;
}
diff --git a/extensions/sql-migration/src/dialog/createMigrationDialog/createMigrationControllerDialog.ts b/extensions/sql-migration/src/dialog/createMigrationDialog/createMigrationControllerDialog.ts
index d5a269cdff..a1d432e321 100644
--- a/extensions/sql-migration/src/dialog/createMigrationDialog/createMigrationControllerDialog.ts
+++ b/extensions/sql-migration/src/dialog/createMigrationDialog/createMigrationControllerDialog.ts
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { createMigrationController, getMigrationControllerRegions, getMigrationController, getResourceGroups, getMigrationControllerAuthKeys, getMigrationControllerMonitoringData } from '../../api/azure';
import { MigrationStateModel } from '../../models/stateMachine';
-import * as constants from '../../models/strings';
+import * as constants from '../../constants/strings';
import * as os from 'os';
import { azureResource } from 'azureResource';
import { IntergrationRuntimePage } from '../../wizard/integrationRuntimePage';
@@ -130,7 +130,6 @@ export class CreateMigrationControllerDialog {
this._dialogObject.okButton.enabled = false;
azdata.window.openDialog(this._dialogObject);
this._dialogObject.cancelButton.onClick((e) => {
- this.migrationStateModel._migrationController = undefined!;
});
this._dialogObject.okButton.onClick((e) => {
this.irPage.populateMigrationController();
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
new file mode 100644
index 0000000000..8346a3e093
--- /dev/null
+++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
@@ -0,0 +1,440 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import { IconPathHelper } from '../../constants/iconPathHelper';
+import { MigrationContext } from '../../models/migrationLocalStorage';
+import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
+import * as loc from '../../constants/strings';
+export class MigrationCutoverDialog {
+ private _dialogObject!: azdata.window.Dialog;
+ private _view!: azdata.ModelView;
+ private _model: MigrationCutoverDialogModel;
+
+ private _databaseTitleName!: azdata.TextComponent;
+ private _databaseCutoverButton!: azdata.ButtonComponent;
+ private _refresh!: azdata.ButtonComponent;
+
+ private _serverName!: azdata.TextComponent;
+ private _serverVersion!: azdata.TextComponent;
+ private _targetServer!: azdata.TextComponent;
+ private _targetVersion!: azdata.TextComponent;
+ private _migrationStatus!: azdata.TextComponent;
+ private _fullBackupFile!: azdata.TextComponent;
+ private _lastAppliedLSN!: azdata.TextComponent;
+ private _lastAppliedBackupFile!: azdata.TextComponent;
+ private _lastAppliedBackupTakenOn!: azdata.TextComponent;
+
+ private _fileCount!: azdata.TextComponent;
+
+ private fileTable!: azdata.TableComponent;
+
+ private _startCutover!: boolean;
+
+ constructor(migration: MigrationContext) {
+ this._model = new MigrationCutoverDialogModel(migration);
+ this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_CUTOVER, 'MigrationCutoverDialog', 1000);
+ }
+
+ async initialize(): Promise {
+ let tab = azdata.window.createTab('');
+ tab.registerContent(async (view: azdata.ModelView) => {
+ this._view = view;
+ const sourceDetails = this.createInfoField(loc.SOURCE_VERSION, '');
+ const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, '');
+
+ this._serverName = sourceDetails.text;
+ this._serverVersion = sourceVersion.text;
+
+ const flexServer = view.modelBuilder.flexContainer().withLayout({
+ flexFlow: 'column'
+ }).component();
+
+ flexServer.addItem(sourceDetails.flexContainer, {
+ CSSStyles: {
+ 'width': '150px'
+ }
+ });
+ flexServer.addItem(sourceVersion.flexContainer, {
+ CSSStyles: {
+ 'width': '150px'
+ }
+ });
+
+ const targetServer = this.createInfoField(loc.TARGET_SERVER, '');
+ const targetVersion = this.createInfoField(loc.TARGET_VERSION, '');
+
+ this._targetServer = targetServer.text;
+ this._targetVersion = targetVersion.text;
+
+ const flexTarget = view.modelBuilder.flexContainer().withLayout({
+ flexFlow: 'column'
+ }).component();
+
+ flexTarget.addItem(targetServer.flexContainer, {
+ CSSStyles: {
+ 'width': '230px'
+ }
+ });
+ flexTarget.addItem(targetVersion.flexContainer, {
+ CSSStyles: {
+ 'width': '230px'
+ }
+ });
+
+ const migrationStatus = this.createInfoField(loc.MIGRATION_STATUS, '');
+ const fullBackupFileOn = this.createInfoField(loc.FULL_BACKUP_FILES, '');
+
+
+ this._migrationStatus = migrationStatus.text;
+ this._fullBackupFile = fullBackupFileOn.text;
+
+ const flexStatus = view.modelBuilder.flexContainer().withLayout({
+ flexFlow: 'column'
+ }).component();
+
+ flexStatus.addItem(migrationStatus.flexContainer, {
+ CSSStyles: {
+ 'width': '180px'
+ }
+ });
+ flexStatus.addItem(fullBackupFileOn.flexContainer, {
+ CSSStyles: {
+ 'width': '180px'
+ }
+ });
+
+ const lastSSN = this.createInfoField(loc.LAST_APPLIED_LSN, '');
+ const lastAppliedBackup = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
+ const lastAppliedBackupOn = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '');
+
+ this._lastAppliedLSN = lastSSN.text;
+ this._lastAppliedBackupFile = lastAppliedBackup.text;
+ this._lastAppliedBackupTakenOn = lastAppliedBackupOn.text;
+
+ const flexFile = view.modelBuilder.flexContainer().withLayout({
+ flexFlow: 'column'
+ }).component();
+ flexFile.addItem(lastSSN.flexContainer, {
+ CSSStyles: {
+ 'width': '230px'
+ }
+ });
+ flexFile.addItem(lastAppliedBackup.flexContainer, {
+ CSSStyles: {
+ 'width': '230px'
+ }
+ });
+ flexFile.addItem(lastAppliedBackupOn.flexContainer, {
+ CSSStyles: {
+ 'width': '230px'
+ }
+ });
+ const flexInfo = view.modelBuilder.flexContainer().withProps({
+ CSSStyles: {
+ 'width': '700px'
+ }
+ }).component();
+
+ flexInfo.addItem(flexServer, {
+ flex: '0',
+ CSSStyles: {
+ 'flex': '0',
+ 'width': '150px'
+ }
+ });
+
+ flexInfo.addItem(flexTarget, {
+ flex: '0',
+ CSSStyles: {
+ 'flex': '0',
+ 'width': '230px'
+ }
+ });
+
+ flexInfo.addItem(flexStatus, {
+ flex: '0',
+ CSSStyles: {
+ 'flex': '0',
+ 'width': '180px'
+ }
+ });
+
+ flexInfo.addItem(flexFile, {
+ flex: '0',
+ CSSStyles: {
+ 'flex': '0',
+ 'width': '200px'
+ }
+ });
+
+ this._fileCount = view.modelBuilder.text().withProps({
+ width: '500px',
+ CSSStyles: {
+ 'font-size': '14px',
+ 'font-weight': 'bold'
+ }
+ }).component();
+
+ this.fileTable = view.modelBuilder.table().withProps({
+ columns: [
+ {
+ value: loc.ACTIVE_BACKUP_FILES,
+ width: 150,
+ type: azdata.ColumnType.text
+ },
+ {
+ value: loc.TYPE,
+ width: 100,
+ type: azdata.ColumnType.text
+ },
+ {
+ value: loc.STATUS,
+ width: 100,
+ type: azdata.ColumnType.text
+ },
+ {
+ value: loc.BACKUP_START_TIME,
+ width: 150,
+ type: azdata.ColumnType.text
+ }, {
+ value: loc.FIRST_LSN,
+ width: 150,
+ type: azdata.ColumnType.text
+ }, {
+ value: loc.LAST_LSN,
+ width: 150,
+ type: azdata.ColumnType.text
+ }
+ ],
+ data: [],
+ width: '800px',
+ height: '600px',
+ }).component();
+
+ const formBuilder = view.modelBuilder.formContainer().withFormItems(
+ [
+ {
+ component: await this.migrationContainerHeader()
+ },
+ {
+ component: flexInfo
+ },
+ {
+ component: this._fileCount
+ },
+ {
+ component: this.fileTable
+ }
+ ],
+ {
+ horizontal: false
+ }
+ );
+ const form = formBuilder.withLayout({ width: '100%' }).component();
+ return view.initializeModel(form);
+ });
+ this._dialogObject.content = [tab];
+ azdata.window.openDialog(this._dialogObject);
+ this.refreshStatus();
+ }
+
+
+ private migrationContainerHeader(): azdata.FlexContainer {
+ const header = this._view.modelBuilder.flexContainer().withLayout({
+ }).component();
+
+ this._databaseTitleName = this._view.modelBuilder.text().withProps({
+ CSSStyles: {
+ 'font-size': 'large',
+ 'width': '400px'
+ },
+ value: this._model._migration.migrationContext.name
+ }).component();
+
+ header.addItem(this._databaseTitleName, {
+ flex: '0',
+ CSSStyles: {
+ 'width': '500px'
+ }
+ });
+
+ this._databaseCutoverButton = this._view.modelBuilder.button().withProps({
+ iconPath: IconPathHelper.cutover,
+ iconHeight: '14px',
+ iconWidth: '12px',
+ label: 'Start Cutover',
+ height: '55px',
+ width: '100px',
+ enabled: false
+ }).component();
+
+ this._databaseCutoverButton.onDidClick(async (e) => {
+ if (this._startCutover) {
+ await this._model.startCutover();
+ this.refreshStatus();
+ } else {
+ this._dialogObject.message = {
+ text: loc.CANNOT_START_CUTOVER_ERROR,
+ level: azdata.window.MessageLevel.Error
+ };
+ }
+ });
+
+ header.addItem(this._databaseCutoverButton, {
+ flex: '0',
+ CSSStyles: {
+ 'width': '100px'
+ }
+ });
+
+ this._refresh = this._view.modelBuilder.button().withProps({
+ iconPath: IconPathHelper.refresh,
+ iconHeight: '16px',
+ iconWidth: '16px',
+ label: 'Refresh',
+ height: '55px',
+ width: '100px'
+ }).component();
+
+ this._refresh.onDidClick((e) => {
+ this.refreshStatus();
+ });
+
+ header.addItem(this._refresh, {
+ flex: '0',
+ CSSStyles: {
+ 'width': '100px'
+ }
+ });
+
+ return header;
+ }
+
+
+ private async refreshStatus(): Promise {
+ try {
+ await this._model.fetchStatus();
+ const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId);
+ const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
+ const sqlServerVersion = sqlServerInfo.serverVersion;
+ const sqlServerEdition = sqlServerInfo.serverEdition;
+ const targetServerName = this._model._migration.targetManagedInstance.name;
+ let targetServerVersion;
+ if (this._model.migrationStatus.id.includes('managedInstances')) {
+ targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
+ } else {
+ targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
+ }
+
+ const migrationStatusTextValue = this._model.migrationStatus.properties.migrationStatus;
+
+ let fullBackupFileName: string;
+ let lastAppliedSSN: string;
+ let lastAppliedBackupFileTakenOn: string;
+
+
+ const tableData: ActiveBackupFileSchema[] = [];
+
+ this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => {
+ tableData.push(
+ {
+ fileName: activeBackupSet.listOfBackupFiles[0].fileName,
+ type: activeBackupSet.backupType,
+ status: activeBackupSet.listOfBackupFiles[0].status,
+ backupStartTime: activeBackupSet.backupStartDate,
+ firstLSN: activeBackupSet.firstLSN,
+ lastLSN: activeBackupSet.lastLSN
+ }
+ );
+ if (activeBackupSet.listOfBackupFiles[0].fileName.substr(activeBackupSet.listOfBackupFiles[0].fileName.lastIndexOf('.') + 1) === 'bak') {
+ fullBackupFileName = activeBackupSet.listOfBackupFiles[0].fileName;
+ }
+ if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
+ lastAppliedSSN = activeBackupSet.lastLSN;
+ lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
+ }
+ });
+
+ this._serverName.value = sqlServerName;
+ this._serverVersion.value = `${sqlServerVersion}
+ ${sqlServerEdition}`;
+
+ this._targetServer.value = targetServerName;
+ this._targetVersion.value = targetServerVersion;
+
+ this._migrationStatus.value = migrationStatusTextValue;
+ this._fullBackupFile.value = fullBackupFileName!;
+
+ this._lastAppliedLSN.value = lastAppliedSSN!;
+ this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
+ this._lastAppliedBackupTakenOn.value = new Date(lastAppliedBackupFileTakenOn!).toLocaleString();
+
+ this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
+
+ this.fileTable.data = tableData.map((row) => {
+ return [
+ row.fileName,
+ row.type,
+ row.status,
+ new Date(row.backupStartTime).toLocaleString(),
+ row.firstLSN,
+ row.lastLSN
+ ];
+ });
+ if (this._model.migrationStatus.properties.migrationStatusDetails?.isFullBackupRestored) {
+ this._startCutover = true;
+ }
+
+ if (migrationStatusTextValue === 'InProgress') {
+ this._databaseCutoverButton.enabled = true;
+ } else {
+ this._databaseCutoverButton.enabled = false;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ private createInfoField(label: string, value: string): {
+ flexContainer: azdata.FlexContainer,
+ text: azdata.TextComponent
+ } {
+ const flexContainer = this._view.modelBuilder.flexContainer().withLayout({
+ flexFlow: 'column'
+ }).component();
+
+ const labelComponent = this._view.modelBuilder.text().withProps({
+ value: label,
+ CSSStyles: {
+ 'font-weight': 'bold',
+ 'margin-bottom': '0'
+ }
+ }).component();
+ flexContainer.addItem(labelComponent);
+
+ const textComponent = this._view.modelBuilder.text().withProps({
+ value: value,
+ CSSStyles: {
+ 'margin-top': '5px',
+ 'margin-bottom': '0'
+ }
+ }).component();
+ flexContainer.addItem(textComponent);
+ return {
+ flexContainer: flexContainer,
+ text: textComponent
+ };
+ }
+}
+
+interface ActiveBackupFileSchema {
+ fileName: string,
+ type: string,
+ status: string,
+ backupStartTime: string,
+ firstLSN: string,
+ lastLSN: string
+}
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
new file mode 100644
index 0000000000..66d5a82dee
--- /dev/null
+++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
@@ -0,0 +1,39 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { getMigrationStatus, DatabaseMigration, startMigrationCutover } from '../../api/azure';
+import { MigrationContext } from '../../models/migrationLocalStorage';
+
+
+export class MigrationCutoverDialogModel {
+
+ public migrationStatus!: DatabaseMigration;
+
+ constructor(public _migration: MigrationContext) {
+ }
+
+ public async fetchStatus(): Promise {
+ this.migrationStatus = (await getMigrationStatus(
+ this._migration.azureAccount,
+ this._migration.subscription,
+ this._migration.migrationContext
+ ));
+ }
+
+ public async startCutover(): Promise {
+ try {
+ if (this.migrationStatus) {
+ return await startMigrationCutover(
+ this._migration.azureAccount,
+ this._migration.subscription,
+ this.migrationStatus
+ );
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ return undefined!;
+ }
+}
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
new file mode 100644
index 0000000000..34d014bdb9
--- /dev/null
+++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
@@ -0,0 +1,248 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import * as vscode from 'vscode';
+import { IconPathHelper } from '../../constants/iconPathHelper';
+import { MigrationContext } from '../../models/migrationLocalStorage';
+import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
+import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel';
+import * as loc from '../../constants/strings';
+export class MigrationStatusDialog {
+ private _model: MigrationStatusDialogModel;
+ private _dialogObject!: azdata.window.Dialog;
+ private _view!: azdata.ModelView;
+ private _searchBox!: azdata.InputBoxComponent;
+ private _refresh!: azdata.ButtonComponent;
+ private _statusDropdown!: azdata.DropDownComponent;
+ private _statusTable!: azdata.DeclarativeTableComponent;
+
+ constructor(migrations: MigrationContext[], private _filter: MigrationCategory) {
+ this._model = new MigrationStatusDialogModel(migrations);
+ this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide');
+ }
+
+ initialize() {
+ let tab = azdata.window.createTab('');
+ tab.registerContent((view: azdata.ModelView) => {
+ this._view = view;
+
+ this._statusDropdown = this._view.modelBuilder.dropDown().withProps({
+ values: this._model.statusDropdownValues,
+ width: '220px'
+ }).component();
+
+ this._statusDropdown.onValueChanged((value) => {
+ this.populateMigrationTable();
+ });
+
+ this._statusDropdown.value = this._statusDropdown.values![this._filter];
+
+ const formBuilder = view.modelBuilder.formContainer().withFormItems(
+ [
+ {
+ component: this.createSearchAndRefreshContainer()
+ },
+ {
+ component: this._statusDropdown
+ },
+ {
+ component: this.createStatusTable()
+ }
+ ],
+ {
+ horizontal: false
+ }
+ );
+ const form = formBuilder.withLayout({ width: '100%' }).component();
+ return view.initializeModel(form);
+ });
+ this._dialogObject.content = [tab];
+ azdata.window.openDialog(this._dialogObject);
+ }
+
+ private createSearchAndRefreshContainer(): azdata.FlexContainer {
+ this._searchBox = this._view.modelBuilder.inputBox().withProps({
+ placeHolder: loc.SEARCH_FOR_MIGRATIONS,
+ width: '360px'
+ }).component();
+
+ this._searchBox.onTextChanged((value) => {
+ this.populateMigrationTable();
+ });
+
+ this._refresh = this._view.modelBuilder.button().withProps({
+ iconPath: {
+ light: IconPathHelper.refresh.light,
+ dark: IconPathHelper.refresh.dark
+ },
+ iconHeight: '16px',
+ iconWidth: '16px',
+ height: '30px',
+ label: 'Refresh',
+ }).component();
+
+ const flexContainer = this._view.modelBuilder.flexContainer().component();
+
+ flexContainer.addItem(this._searchBox, {
+ flex: '0'
+ });
+
+ flexContainer.addItem(this._refresh, {
+ flex: '0',
+ CSSStyles: {
+ 'margin-left': '20px'
+ }
+ });
+
+ return flexContainer;
+ }
+
+ private populateMigrationTable(): void {
+
+ try {
+ const migrations = this._model.filterMigration(
+ this._searchBox.value!,
+ (this._statusDropdown.value).name
+ );
+
+ const data: azdata.DeclarativeTableCellValue[][] = [];
+
+ migrations.forEach((migration) => {
+ const migrationRow: azdata.DeclarativeTableCellValue[] = [];
+
+ const databaseHyperLink = this._view.modelBuilder.hyperlink().withProps({
+ label: migration.migrationContext.name,
+ url: ''
+ }).component();
+ databaseHyperLink.onDidClick(async (e) => {
+ await (new MigrationCutoverDialog(migration)).initialize();
+ });
+ migrationRow.push({
+ value: databaseHyperLink,
+ });
+
+ migrationRow.push({
+ value: migration.migrationContext.properties.migrationStatus
+ });
+
+ const sqlMigrationIcon = this._view.modelBuilder.image().withProps({
+ iconPath: IconPathHelper.sqlMigrationLogo,
+ iconWidth: '16px',
+ iconHeight: '16px',
+ width: '32px',
+ height: '20px'
+ }).component();
+ const sqlMigrationName = this._view.modelBuilder.hyperlink().withProps({
+ label: migration.migrationContext.name,
+ url: ''
+ }).component();
+ sqlMigrationName.onDidClick((e) => {
+ vscode.window.showInformationMessage('Feature coming soon');
+ });
+
+ const sqlMigrationContainer = this._view.modelBuilder.flexContainer().withProps({
+ CSSStyles: {
+ 'justify-content': 'center'
+ }
+ }).component();
+ sqlMigrationContainer.addItem(sqlMigrationIcon, {
+ flex: '0',
+ CSSStyles: {
+ 'width': '32px'
+ }
+ });
+ sqlMigrationContainer.addItem(sqlMigrationName,
+ {
+ CSSStyles: {
+ 'width': 'auto'
+ }
+ });
+ migrationRow.push({
+ value: sqlMigrationContainer
+ });
+
+ migrationRow.push({
+ value: loc.ONLINE
+ });
+
+ migrationRow.push({
+ value: '---'
+ });
+ migrationRow.push({
+ value: '---'
+ });
+
+ data.push(migrationRow);
+ });
+
+ this._statusTable.dataValues = data;
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ private createStatusTable(): azdata.DeclarativeTableComponent {
+ this._statusTable = this._view.modelBuilder.declarativeTable().withProps({
+ columns: [
+ {
+ displayName: loc.DATABASE,
+ valueType: azdata.DeclarativeDataType.component,
+ width: '100px',
+ isReadOnly: true,
+ rowCssStyles: {
+ 'text-align': 'center'
+ }
+ },
+ {
+ displayName: loc.MIGRATION_STATUS,
+ valueType: azdata.DeclarativeDataType.string,
+ width: '150px',
+ isReadOnly: true,
+ rowCssStyles: {
+ 'text-align': 'center'
+ }
+ },
+ {
+ displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
+ valueType: azdata.DeclarativeDataType.component,
+ width: '300px',
+ isReadOnly: true,
+ rowCssStyles: {
+ 'text-align': 'center'
+ }
+ },
+ {
+ displayName: loc.CUTOVER_TYPE,
+ valueType: azdata.DeclarativeDataType.string,
+ width: '100px',
+ isReadOnly: true,
+ rowCssStyles: {
+ 'text-align': 'center'
+ }
+ },
+ {
+ displayName: loc.START_TIME,
+ valueType: azdata.DeclarativeDataType.string,
+ width: '150px',
+ isReadOnly: true,
+ rowCssStyles: {
+ 'text-align': 'center'
+ }
+ },
+ {
+ displayName: loc.FINISH_TIME,
+ valueType: azdata.DeclarativeDataType.string,
+ width: '150px',
+ isReadOnly: true,
+ rowCssStyles: {
+ 'text-align': 'center'
+ }
+ }
+ ]
+ }).component();
+ return this._statusTable;
+ }
+}
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
new file mode 100644
index 0000000000..a058ee514d
--- /dev/null
+++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
@@ -0,0 +1,56 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import { MigrationContext } from '../../models/migrationLocalStorage';
+
+export class MigrationStatusDialogModel {
+
+ public statusDropdownValues: azdata.CategoryValue[] = [
+ {
+ displayName: 'Status: All',
+ name: 'All',
+ }, {
+ displayName: 'Status: Ongoing',
+ name: 'Ongoing',
+ }, {
+ displayName: 'Status: Succeeded',
+ name: 'Succeeded',
+ }
+ ];
+
+ 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;
+ return status === 'InProgress' || status === 'Creating' || status === 'Completing';
+ });
+ } 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
+}
diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts
index be6a5e4a9e..7ba7651950 100644
--- a/extensions/sql-migration/src/main.ts
+++ b/extensions/sql-migration/src/main.ts
@@ -6,9 +6,8 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { WizardController } from './wizard/wizardController';
-import { AssessmentResultsDialog } from './dialog/assessmentResults/assessmentResultsDialog';
import { promises as fs } from 'fs';
-import * as loc from './models/strings';
+import * as loc from './constants/strings';
import { MigrationNotebookInfo, NotebookPathHelper } from './constants/notebookPathHelper';
import { IconPathHelper } from './constants/iconPathHelper';
import { DashboardWidget } from './dashboard/sqlServerDashboard';
@@ -42,12 +41,6 @@ class SQLMigration {
const wizardController = new WizardController(this.context);
await wizardController.openWizard(connectionId);
}),
-
- vscode.commands.registerCommand('sqlmigration.testDialog', async () => {
- let dialog = new AssessmentResultsDialog('ownerUri', undefined!, 'Assessment Dialog');
- await dialog.openDialog();
- }),
-
vscode.commands.registerCommand('sqlmigration.openNotebooks', async () => {
const input = vscode.window.createQuickPick();
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts
index ce85361b32..5388bc4b2f 100644
--- a/extensions/sql-migration/src/models/migrationLocalStorage.ts
+++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { azureResource } from 'azureResource';
-import { DatabaseMigration, MigrationController, SqlManagedInstance } from '../api/azure';
+import { DatabaseMigration, SqlMigrationController, SqlManagedInstance } from '../api/azure';
import * as azdata from 'azdata';
@@ -16,7 +16,7 @@ export class MigrationLocalStorage {
MigrationLocalStorage.context = context;
}
- public static getMigrations(connectionProfile: azdata.connection.ConnectionProfile): MigrationContext[] {
+ public static getMigrationsBySourceConnections(connectionProfile: azdata.connection.ConnectionProfile): MigrationContext[] {
let dataBaseMigrations: MigrationContext[] = [];
try {
@@ -41,7 +41,7 @@ export class MigrationLocalStorage {
targetMI: SqlManagedInstance,
azureAccount: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
- controller: MigrationController): void {
+ controller: SqlMigrationController): void {
try {
const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
migrationMementos.push({
@@ -69,5 +69,5 @@ export interface MigrationContext {
targetManagedInstance: SqlManagedInstance,
azureAccount: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
- controller: MigrationController
+ controller: SqlMigrationController
}
diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts
index 94423c46c2..e27abc279e 100644
--- a/extensions/sql-migration/src/models/stateMachine.ts
+++ b/extensions/sql-migration/src/models/stateMachine.ts
@@ -7,9 +7,9 @@ import * as azdata from 'azdata';
import { azureResource } from 'azureResource';
import * as vscode from 'vscode';
import * as mssql from '../../../mssql';
-import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, MigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount } from '../api/azure';
+import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, SqlMigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount } from '../api/azure';
import { SKURecommendations } from './externalContract';
-import * as constants from '../models/strings';
+import * as constants from '../constants/strings';
import { MigrationLocalStorage } from './migrationLocalStorage';
export enum State {
@@ -85,13 +85,13 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _targetManagedInstance!: SqlManagedInstance;
public _databaseBackup!: DatabaseBackupModel;
- public _migrationDbs!: string[];
+ public _migrationDbs: string[] = [];
public _storageAccounts!: StorageAccount[];
public _fileShares!: azureResource.FileShare[];
public _blobContainers!: azureResource.BlobContainer[];
- public _migrationController!: MigrationController;
- public _migrationControllers!: MigrationController[];
+ public _migrationController!: SqlMigrationController;
+ public _migrationControllers!: SqlMigrationController[];
public _nodeNames!: string[];
private _stateChangeEventEmitter = new vscode.EventEmitter();
@@ -403,7 +403,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
return migrationControllerValues;
}
- public getMigrationController(index: number): MigrationController {
+ public getMigrationController(index: number): SqlMigrationController {
return this._migrationControllers[index];
}
@@ -421,7 +421,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
const requestBody: StartDatabaseMigrationRequest = {
location: this._migrationController?.properties.location!,
properties: {
- SourceDatabaseName: currentConnection?.databaseName!,
+ SourceDatabaseName: '',
MigrationController: this._migrationController?.id!,
BackupConfiguration: {
TargetLocation: {
@@ -445,26 +445,36 @@ export class MigrationStateModel implements Model, vscode.Disposable {
}
};
- const response = await startDatabaseMigration(
- this._azureAccount,
- this._targetSubscription,
- this._targetManagedInstance.resourceGroup!,
- this._migrationController?.properties.location!,
- this._targetManagedInstance.name,
- this._migrationController?.name!,
- requestBody
- );
+ this._migrationDbs.forEach(async (db) => {
- if (response.status === 201) {
- MigrationLocalStorage.saveMigration(
- currentConnection!,
- response.databaseMigration,
- this._targetManagedInstance,
- this._azureAccount,
- this._targetSubscription,
- this._migrationController
- );
- }
+ requestBody.properties.SourceDatabaseName = db;
+ try {
+ const response = await startDatabaseMigration(
+ this._azureAccount,
+ this._targetSubscription,
+ this._targetManagedInstance.resourceGroup!,
+ this._migrationController?.properties.location!,
+ this._targetManagedInstance.name,
+ currentConnection?.databaseName!,
+ requestBody
+ );
+
+ if (response.status === 201) {
+ MigrationLocalStorage.saveMigration(
+ currentConnection!,
+ response.databaseMigration,
+ this._targetManagedInstance,
+ this._azureAccount,
+ this._targetSubscription,
+ this._migrationController
+ );
+ vscode.window.showInformationMessage(`Starting migration for database ${db} to ${this._targetManagedInstance.name}`);
+ }
+ } catch (e) {
+ vscode.window.showInformationMessage(e);
+ }
+
+ });
vscode.window.showInformationMessage(constants.MIGRATION_STARTED);
}
diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts
index 9037bd9573..015907f4d4 100644
--- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts
+++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
-import * as constants from '../models/strings';
+import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
export class AccountsSelectionPage extends MigrationWizardPage {
diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts
index 7903e9730f..46f8cdac32 100644
--- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts
+++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts
@@ -8,8 +8,8 @@ import { EOL } from 'os';
import { getStorageAccountAccessKeys } from '../api/azure';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationCutover, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
-import * as constants from '../models/strings';
-
+import * as constants from '../constants/strings';
+import * as vscode from 'vscode';
export class DatabaseBackupPage extends MigrationWizardPage {
private _networkShareContainer!: azdata.FlexContainer;
@@ -85,7 +85,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
blobContainerButton.onDidChangeCheckedState((e) => {
if (e) {
- this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
+ vscode.window.showInformationMessage('Feature coming soon');
+ networkShareButton.checked = true;
+ //this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
}
});
@@ -97,7 +99,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
fileShareButton.onDidChangeCheckedState((e) => {
if (e) {
- this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE);
+ vscode.window.showInformationMessage('Feature coming soon');
+ networkShareButton.checked = true;
+ //this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE);
}
});
@@ -414,7 +418,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
offlineButton.onDidChangeCheckedState((e) => {
if (e) {
- this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.OFFLINE;
+ vscode.window.showInformationMessage('Feature coming soon');
+ onlineButton.checked = true;
+ //this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.OFFLINE;
}
});
@@ -489,7 +495,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
public async onPageLeave(): Promise {
this.migrationStateModel._databaseBackup.storageKey = (await getStorageAccountAccessKeys(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount)).keyName1;
- console.log(this.migrationStateModel._databaseBackup);
+ this.wizard.registerNavigationValidator((pageChangeInfo) => {
+ return true;
+ });
}
protected async handleStateChange(e: StateChangeEvent): Promise {
diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts
index 2e9285b230..96fc8b49e7 100644
--- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts
+++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts
@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { CreateMigrationControllerDialog } from '../dialog/createMigrationDialog/createMigrationControllerDialog';
-import * as constants from '../models/strings';
+import * as constants from '../constants/strings';
import { createInformationRow, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import { getMigrationController, getMigrationControllerAuthKeys, getMigrationControllerMonitoringData } from '../api/azure';
import { IconPathHelper } from '../constants/iconPathHelper';
@@ -147,7 +147,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
return flexContainer;
}
- public async populateMigrationController(controllerStatus?: string): Promise {
+ public async populateMigrationController(): Promise {
this.migrationControllerDropdown.loading = true;
try {
this.migrationControllerDropdown.values = await this.migrationStateModel.getMigrationControllerValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetManagedInstance);
diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts
index e78aea889e..ebe84dada8 100644
--- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts
+++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts
@@ -8,11 +8,10 @@ import * as path from 'path';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { Product, ProductLookupTable } from '../models/product';
-import { Disposable } from 'vscode';
import { AssessmentResultsDialog } from '../dialog/assessmentResults/assessmentResultsDialog';
-import { getAvailableManagedInstanceProducts, getSubscriptions, SqlManagedInstance, Subscription } from '../api/azure';
-import * as constants from '../models/strings';
-import { azureResource } from 'azureResource';
+import * as constants from '../constants/strings';
+import * as vscode from 'vscode';
+import { EOL } from 'os';
// import { SqlMigrationService } from '../../../../extensions/mssql/src/sqlMigration/sqlMigrationService';
@@ -32,11 +31,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
private _azureSubscriptionText: azdata.FormComponent | undefined;
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
private _managedInstanceDropdown!: azdata.DropDownComponent;
- private _subscriptionDropdownValues: azdata.CategoryValue[] = [];
- private _subscriptionMap: Map = new Map();
private _view: azdata.ModelView | undefined;
+ private _rbg!: azdata.RadioCardGroupComponent;
private async initialState(view: azdata.ModelView) {
+ this._view = view;
this._igComponent = this.createStatusComponent(view); // The first component giving basic information
this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved
this._chooseTargetComponent = this.createChooseTargetComponent(view);
@@ -47,12 +46,24 @@ export class SKURecommendationPage extends MigrationWizardPage {
}).component();
this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().component();
this._managedInstanceSubscriptionDropdown.onValueChanged((e) => {
- this.populateManagedInstanceDropdown();
+ if (e.selected) {
+ this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index);
+ this.migrationStateModel._targetManagedInstance = undefined!;
+ this.migrationStateModel._migrationController = undefined!;
+ this.populateManagedInstanceDropdown();
+ }
});
const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({
value: constants.MANAGED_INSTANCE
}).component();
+
this._managedInstanceDropdown = view.modelBuilder.dropDown().component();
+ this._managedInstanceDropdown.onValueChanged((e) => {
+ if (e.selected) {
+ this.migrationStateModel._migrationControllers = undefined!;
+ this.migrationStateModel._targetManagedInstance = this.migrationStateModel.getManagedInstance(e.index);
+ }
+ });
const targetContainer = view.modelBuilder.flexContainer().withItems(
[
@@ -137,18 +148,23 @@ export class SKURecommendationPage extends MigrationWizardPage {
private constructTargets(): void {
const products: Product[] = Object.values(ProductLookupTable);
- const rbg = this._view!.modelBuilder.radioCardGroup().withProperties({
+ this._rbg = this._view!.modelBuilder.radioCardGroup().withProperties({
cards: [],
cardWidth: '600px',
cardHeight: '60px',
orientation: azdata.Orientation.Vertical,
iconHeight: '30px',
iconWidth: '30px'
- });
+ }).component();
products.forEach((product) => {
const imagePath = path.resolve(this.migrationStateModel.getExtensionPath(), 'media', product.icon ?? 'ads.svg');
-
+ let dbCount = 0;
+ if (product.type === 'AzureSQLVM') {
+ dbCount = 0;
+ } else {
+ dbCount = this.migrationStateModel._migrationDbs.length;
+ }
const descriptions: azdata.RadioCardDescription[] = [
{
textValue: product.name,
@@ -164,7 +180,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
},
{
- textValue: '9 databases will be migrated',
+ textValue: `${dbCount} databases will be migrated`,
linkDisplayValue: 'View/Change',
displayLinkCodicon: true,
linkCodiconStyles: {
@@ -174,22 +190,31 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
];
- rbg.component().cards.push({
- id: product.name,
+ this._rbg.cards.push({
+ id: product.type,
icon: imagePath,
descriptions
});
});
- rbg.component().onLinkClick(async (value) => {
+ this._rbg.onLinkClick(async (value) => {
//check which card is being selected, and open correct dialog based on link
console.log(value);
- let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog');
+ let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog', this);
await dialog.openDialog();
});
- this._chooseTargetComponent?.component.addItem(rbg.component());
+ this._rbg.onSelectionChanged((value) => {
+ if (value.cardId === 'AzureSQLVM') {
+ vscode.window.showInformationMessage('Feature coming soon');
+ this._rbg.selectedCardId = 'AzureSQLMI';
+ }
+ });
+
+ this._rbg.selectedCardId = 'AzureSQLMI';
+
+ this._chooseTargetComponent?.component.addItem(this._rbg);
}
private createAzureSubscriptionText(view: azdata.ModelView): azdata.FormComponent {
@@ -205,85 +230,78 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private async populateSubscriptionDropdown(): Promise {
- this._managedInstanceSubscriptionDropdown.loading = true;
- this._managedInstanceDropdown.loading = true;
- let subscriptions: azureResource.AzureResourceSubscription[] = [];
- try {
- subscriptions = await getSubscriptions(this.migrationStateModel._azureAccount);
- subscriptions.forEach((subscription) => {
- this._subscriptionMap.set(subscription.id, subscription);
- this._subscriptionDropdownValues.push({
- name: subscription.id,
- displayName: subscription.name + ' - ' + subscription.id,
- });
- });
-
- if (!this._subscriptionDropdownValues || this._subscriptionDropdownValues.length === 0) {
- this._subscriptionDropdownValues = [
- {
- displayName: constants.NO_SUBSCRIPTIONS_FOUND,
- name: ''
- }
- ];
+ if (!this.migrationStateModel._targetSubscription) {
+ this._managedInstanceSubscriptionDropdown.loading = true;
+ this._managedInstanceDropdown.loading = true;
+ try {
+ this._managedInstanceSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues();
+ } catch (e) {
+ console.log(e);
+ } finally {
+ this._managedInstanceSubscriptionDropdown.loading = false;
}
-
- this._managedInstanceSubscriptionDropdown.values = this._subscriptionDropdownValues;
- } catch (error) {
- this.setEmptyDropdownPlaceHolder(this._managedInstanceSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
- this._managedInstanceDropdown.loading = false;
}
- this.populateManagedInstanceDropdown();
- this._managedInstanceSubscriptionDropdown.loading = false;
}
private async populateManagedInstanceDropdown(): Promise {
- this._managedInstanceDropdown.loading = true;
- let mis: SqlManagedInstance[] = [];
- let miValues: azdata.CategoryValue[] = [];
- try {
- const subscriptionId = (this._managedInstanceSubscriptionDropdown.value).name;
-
- mis = await getAvailableManagedInstanceProducts(this.migrationStateModel._azureAccount, this._subscriptionMap.get(subscriptionId)!);
- mis.forEach((mi) => {
- miValues.push({
- name: mi.name,
- displayName: mi.name
- });
- });
-
- if (!miValues || miValues.length === 0) {
- miValues = [
- {
- displayName: constants.NO_MANAGED_INSTANCE_FOUND,
- name: ''
- }
- ];
+ if (!this.migrationStateModel._targetManagedInstance) {
+ this._managedInstanceDropdown.loading = true;
+ try {
+ this._managedInstanceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription);
+ } catch (e) {
+ console.log(e);
+ } finally {
+ this._managedInstanceDropdown.loading = false;
}
-
- this._managedInstanceDropdown.values = miValues;
- } catch (error) {
- this.setEmptyDropdownPlaceHolder(this._managedInstanceDropdown, constants.NO_MANAGED_INSTANCE_FOUND);
}
-
- this._managedInstanceDropdown.loading = false;
}
- private setEmptyDropdownPlaceHolder(dropDown: azdata.DropDownComponent, placeholder: string): void {
- dropDown.values = [{
- displayName: placeholder,
- name: ''
- }];
- }
-
- private eventListener: Disposable | undefined;
+ private eventListener: vscode.Disposable | undefined;
public async onPageEnter(): Promise {
this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e));
this.populateSubscriptionDropdown();
this.constructDetails();
+
+ this.wizard.registerNavigationValidator((pageChangeInfo) => {
+ const errors: string[] = [];
+ this.wizard.message = {
+ text: '',
+ level: azdata.window.MessageLevel.Error
+ };
+ if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
+ return true;
+ }
+ if (this.migrationStateModel._migrationDbs.length === 0) {
+ errors.push('Please select databases to migrate');
+
+ }
+ if ((this._managedInstanceSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
+ errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
+ }
+ if ((this._managedInstanceDropdown.value).displayName === constants.NO_MANAGED_INSTANCE_FOUND) {
+ errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
+ }
+
+ if (errors.length > 0) {
+ this.wizard.message = {
+ text: errors.join(EOL),
+ level: azdata.window.MessageLevel.Error
+ };
+ return false;
+ }
+ return true;
+ });
}
public async onPageLeave(): Promise {
this.eventListener?.dispose();
+ this.wizard.message = {
+ text: '',
+ level: azdata.window.MessageLevel.Error
+ };
+ this.wizard.registerNavigationValidator((pageChangeInfo) => {
+ return true;
+ });
}
protected async handleStateChange(e: StateChangeEvent): Promise {
@@ -292,4 +310,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
}
+ public refreshDatabaseCount(count: number): void {
+ this.wizard.message = {
+ text: '',
+ level: azdata.window.MessageLevel.Error
+ };
+ const textValue: string = `${count} databases will be migrated`;
+ this._rbg.cards[0].descriptions[1].textValue = textValue;
+ this._rbg.updateProperties({
+ cards: this._rbg.cards
+ });
+ }
+
}
diff --git a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts
index 75e6a11a26..4d323053f0 100644
--- a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts
+++ b/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts
@@ -5,7 +5,7 @@
import * as azdata from 'azdata';
import { MigrationWizardPage } from '../models/migrationWizardPage';
-import { SOURCE_CONFIGURATION_PAGE_TITLE, COLLECTING_SOURCE_CONFIGURATIONS, COLLECTING_SOURCE_CONFIGURATIONS_INFO, COLLECTING_SOURCE_CONFIGURATIONS_ERROR } from '../models/strings';
+import { SOURCE_CONFIGURATION_PAGE_TITLE, COLLECTING_SOURCE_CONFIGURATIONS, COLLECTING_SOURCE_CONFIGURATIONS_INFO, COLLECTING_SOURCE_CONFIGURATIONS_ERROR } from '../constants/strings';
import { MigrationStateModel, StateChangeEvent, State } from '../models/stateMachine';
import { Disposable } from 'vscode';
diff --git a/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts b/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts
index fae6011764..d5621398f0 100644
--- a/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts
+++ b/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts
@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
-import { SUBSCRIPTION_SELECTION_PAGE_TITLE, SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE, SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE, SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE } from '../models/strings';
+import { SUBSCRIPTION_SELECTION_PAGE_TITLE, SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE, SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE, SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE } from '../constants/strings';
import { Disposable } from 'vscode';
import { getSubscriptions, Subscription, getAvailableManagedInstanceProducts, AzureProduct, getAvailableSqlServers } from '../api/azure';
diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts
index 0dd97980d6..816734cc02 100644
--- a/extensions/sql-migration/src/wizard/summaryPage.ts
+++ b/extensions/sql-migration/src/wizard/summaryPage.ts
@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
-import * as constants from '../models/strings';
+import * as constants from '../constants/strings';
import { createHeadingTextComponent, createInformationRow } from './wizardController';
export class SummaryPage extends MigrationWizardPage {
@@ -42,7 +42,7 @@ export class SummaryPage extends MigrationWizardPage {
createInformationRow(this._view, constants.TYPE, constants.SUMMARY_MI_TYPE),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.SUMMARY_MI_TYPE, this.migrationStateModel._targetManagedInstance.name),
- createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, '1'),
+ createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()),
createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE),
this.createNetworkContainerRows(),
createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE),
diff --git a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts
index 9cc9384855..7a5cd8ab8a 100644
--- a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts
+++ b/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts
@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
-import * as constants from '../models/strings';
+import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
export class TempTargetSelectionPage extends MigrationWizardPage {
diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts
index 70b1e92c12..03d0939db0 100644
--- a/extensions/sql-migration/src/wizard/wizardController.ts
+++ b/extensions/sql-migration/src/wizard/wizardController.ts
@@ -6,7 +6,7 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as mssql from '../../../mssql';
import { MigrationStateModel } from '../models/stateMachine';
-import { WIZARD_TITLE } from '../models/strings';
+import * as loc from '../constants/strings';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { SKURecommendationPage } from './skuRecommendationPage';
// import { SubscriptionSelectionPage } from './subscriptionSelectionPage';
@@ -31,7 +31,7 @@ export class WizardController {
}
private async createWizard(stateModel: MigrationStateModel): Promise {
- const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide');
+ const wizard = azdata.window.createWizard(loc.WIZARD_TITLE, 'wide');
wizard.generateScriptButton.enabled = false;
wizard.generateScriptButton.hidden = true;
const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel);