Adding migration status and cutover to extension (#14482)

This commit is contained in:
Aasim Khan
2021-03-02 17:11:17 -08:00
committed by GitHub
parent 1e67388653
commit f2ae5419bb
33 changed files with 1452 additions and 236 deletions

View File

@@ -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<azurecore.IExtension> {
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<MigrationController> {
export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise<SqlMigrationController> {
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<MigrationController[]> {
export async function getMigrationControllers(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string): Promise<SqlMigrationController[]> {
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<MigrationController> {
export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise<SqlMigrationController> {
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<StartDatabaseMigrationResponse> {
export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest): Promise<StartDatabaseMigrationResponse> {
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<any> {
export async function getDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, migrationId: string): Promise<DatabaseMigration> {
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<DatabaseMigration> {
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<DatabaseMigration[]> {
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<any> {
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;
}

View File

@@ -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')
};
}
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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(<string>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<void> {
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<MigrationContext[]> {
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<azdata.Component> {
@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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<void> {
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<void> {
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
}

View File

@@ -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<void> {
this.migrationStatus = (await getMigrationStatus(
this._migration.azureAccount,
this._migration.subscription,
this._migration.migrationContext
));
}
public async startCutover(): Promise<DatabaseMigration | undefined> {
try {
if (this.migrationStatus) {
return await startMigrationCutover(
this._migration.azureAccount,
this._migration.subscription,
this.migrationStatus
);
}
} catch (error) {
console.log(error);
}
return undefined!;
}
}

View File

@@ -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!,
(<azdata.CategoryValue>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;
}
}

View File

@@ -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
}

View File

@@ -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<MigrationNotebookInfo>();
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;

View File

@@ -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
}

View File

@@ -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<StateChangeEvent>();
@@ -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);
}

View File

@@ -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 {

View File

@@ -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<void> {
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<void> {

View File

@@ -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<void> {
public async populateMigrationController(): Promise<void> {
this.migrationControllerDropdown.loading = true;
try {
this.migrationControllerDropdown.values = await this.migrationStateModel.getMigrationControllerValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetManagedInstance);

View File

@@ -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<azdata.TextComponent> | undefined;
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
private _managedInstanceDropdown!: azdata.DropDownComponent;
private _subscriptionDropdownValues: azdata.CategoryValue[] = [];
private _subscriptionMap: Map<string, Subscription> = 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<azdata.RadioCardGroupComponentProperties>({
this._rbg = this._view!.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
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<azdata.TextComponent> {
@@ -205,85 +230,78 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private async populateSubscriptionDropdown(): Promise<void> {
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<void> {
this._managedInstanceDropdown.loading = true;
let mis: SqlManagedInstance[] = [];
let miValues: azdata.CategoryValue[] = [];
try {
const subscriptionId = (<azdata.CategoryValue>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<void> {
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 ((<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
}
if ((<azdata.CategoryValue>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<void> {
this.eventListener?.dispose();
this.wizard.message = {
text: '',
level: azdata.window.MessageLevel.Error
};
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
});
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
@@ -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
});
}
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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<void> {
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);