mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-21 17:22:55 -05:00
Adding windows auth support to sql-migration and misc bug fixes. (#14816)
* - Added coming soon message for learn more. - Potential fix for learn more message * Renaming of controller to sqlMigrationService * Surfacing some errors -Azure account is stale error -Migration Service creation error. * Adding refresh azure token validation. * Fixing some errors pointed during PR -Fixing property names -Fixing count * Fixing migration status - Adding special error handling for resource not found error - Deleting unfound migrations from local cache - Using prefetched migration status for view all Misc fixes: - Using SQL server version name instead of number - Fixing Icons on sku recommendation page - Fixing table column width in cutover dialog - Adding spinner button to refresh. * Fixing all strings in migration service page and dialog * fixed a string error in create service dialog * Adding source config page to migration to support windows auth Some refactorings for sqlDatabaseTree (still WIP) * refactoring assessments code 1 introducing new interface for server assessments * Filtering out non windows sql vms Retaining selections made by user on assessments dialog * Fix compile errors on sqlDatabaseTree * Exposing migration status errors in cutover dialog * Updating extension verion * Correcting typos Fixing compilation erros Removing en-us from url Fixing function names Make UI calls unblocking * Unblocking dialog in case of failed assessments Localizing string removing blocking code from UI Fixing comments * Fixed broken assessment page logic
This commit is contained in:
@@ -50,7 +50,21 @@ export async function getAvailableSqlServers(account: azdata.Account, subscripti
|
||||
return result.resources;
|
||||
}
|
||||
|
||||
export type SqlVMServer = AzureProduct;
|
||||
export type SqlVMServer = {
|
||||
properties: {
|
||||
virtualMachineResourceId: string,
|
||||
provisioningState: string,
|
||||
sqlImageOffer: string,
|
||||
sqlManagement: string,
|
||||
sqlImageSku: string
|
||||
},
|
||||
location: string,
|
||||
id: string,
|
||||
name: string,
|
||||
type: string,
|
||||
tenantId: string,
|
||||
subscriptionId: string
|
||||
};
|
||||
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const path = `/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`;
|
||||
@@ -331,6 +345,7 @@ export interface StartDatabaseMigrationRequest {
|
||||
},
|
||||
},
|
||||
sourceSqlConnection: {
|
||||
authentication: string,
|
||||
dataSource: string,
|
||||
username: string,
|
||||
password: string
|
||||
|
||||
@@ -29,9 +29,10 @@ export const SKU_RECOMMENDATION_SOME_SUCCESSFUL = (migratableCount: number, data
|
||||
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose a target Azure SQL");
|
||||
|
||||
export const SKU_RECOMMENDATION_NONE_SUCCESSFUL = localize('sql.migration.sku.none', "Based on the results of our source configuration scans, none of your databases can be migrated to Azure SQL.");
|
||||
export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure Managed Instance (Microsoft managed)");
|
||||
export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "Azure SQL Virtual Machine (Customer managed)'");
|
||||
|
||||
export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure Managed Instance (PaaS)");
|
||||
export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "Azure SQL Virtual Machine (IaaS)");
|
||||
export const SELECT_AZURE_MI = localize('sql.migration.select.azure.mi', "Select an Azure subscription and an Azure SQL Managed Instance for your target.");
|
||||
export const SELECT_AZURE_VM = localize('sql.migration.select.azure.vm', "Select an Azure subscription and an Azure SQL Virtual Machine for your target.");
|
||||
export const SUBSCRIPTION_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.subscription.title', "Azure Subscription Selection");
|
||||
export const SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE = localize('sql.migration.wizard.subscription.azure.account.title', "Azure Account");
|
||||
export const SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE = localize('sql.migration.wizard.subscription.azure.subscription.title', "Azure Subscription");
|
||||
@@ -40,6 +41,7 @@ export const SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE = localize('sql.migratio
|
||||
export const ASSESSMENT_COMPLETED = (serverName: string): string => {
|
||||
return localize('sql.migration.generic.congratulations', "We have completed the assessment of your SQL Server Instance '{0}'.", serverName);
|
||||
};
|
||||
export const ASSESSMENT_TILE = localize('sql.migration.assessment', "Assessment Dialog");
|
||||
|
||||
// Accounts page
|
||||
export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Azure Account");
|
||||
@@ -264,3 +266,11 @@ export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azu
|
||||
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");
|
||||
|
||||
//Source Credentials page.
|
||||
export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration', "Source Configuration");
|
||||
export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source Credentials");
|
||||
export function ENTER_YOUR_SQL_CREDS(sqlServerName: string) {
|
||||
return localize('sql.migration.enter.your.sql.creds', "Enter the credentials for source SQL server instance ‘{0}’", sqlServerName);
|
||||
}
|
||||
export const USERNAME = localize('sql.migration.username', "Username");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationStateModel } from '../../models/stateMachine';
|
||||
import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
|
||||
import { SqlDatabaseTree } from './sqlDatabasesTree';
|
||||
import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
|
||||
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
|
||||
@@ -14,7 +14,6 @@ export type Issues = {
|
||||
recommendation: string,
|
||||
moreInfo: string,
|
||||
impactedObjects: SqlMigrationImpactedObjectInfo[],
|
||||
rowNumber: number
|
||||
};
|
||||
export class AssessmentResultsDialog {
|
||||
|
||||
@@ -31,10 +30,9 @@ export class AssessmentResultsDialog {
|
||||
private _tree: SqlDatabaseTree;
|
||||
|
||||
|
||||
constructor(public ownerUri: string, public model: MigrationStateModel, public title: string, private skuRecommendationPage: SKURecommendationPage, migrationType: string) {
|
||||
constructor(public ownerUri: string, public model: MigrationStateModel, public title: string, private _skuRecommendationPage: SKURecommendationPage, private _targetType: MigrationTargetType) {
|
||||
this._model = model;
|
||||
let assessmentData = this.parseData(this._model);
|
||||
this._tree = new SqlDatabaseTree(this._model, assessmentData, migrationType);
|
||||
this._tree = new SqlDatabaseTree(this._model, this._targetType);
|
||||
}
|
||||
|
||||
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
||||
@@ -42,8 +40,7 @@ export class AssessmentResultsDialog {
|
||||
dialog.registerContent(async (view) => {
|
||||
try {
|
||||
const resultComponent = await this._tree.createComponentResult(view);
|
||||
const treeComponent = await this._tree.createComponent(view);
|
||||
|
||||
const treeComponent = await this._tree.createComponent(view, this._targetType === MigrationTargetType.SQLVM ? this.model._vmDbs : this._model._miDbs);
|
||||
const flex = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
height: '100%',
|
||||
@@ -79,55 +76,22 @@ export class AssessmentResultsDialog {
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
|
||||
azdata.window.openDialog(this.dialog);
|
||||
|
||||
await Promise.all(dialogSetupPromises);
|
||||
|
||||
await this._tree.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private parseData(model: MigrationStateModel): Map<string, Issues[]> {
|
||||
// if there are multiple issues for the same DB, need to consolidate
|
||||
// map DB name -> Assessment result items (issues)
|
||||
// map assessment result items to description, recommendation, more info & impacted objects
|
||||
|
||||
let dbMap = new Map<string, Issues[]>();
|
||||
|
||||
model.assessmentResults?.forEach((element) => {
|
||||
let issues: Issues;
|
||||
issues = {
|
||||
description: element.description,
|
||||
recommendation: element.message,
|
||||
moreInfo: element.helpLink,
|
||||
impactedObjects: element.impactedObjects,
|
||||
rowNumber: 0
|
||||
};
|
||||
if (element.targetName.includes(':')) {
|
||||
let spliceIndex = element.targetName.indexOf(':');
|
||||
let dbName = element.targetName.slice(spliceIndex + 1);
|
||||
let dbIssues = dbMap.get(element.targetName);
|
||||
if (dbIssues) {
|
||||
dbMap.set(dbName, dbIssues.concat([issues]));
|
||||
} else {
|
||||
dbMap.set(dbName, [issues]);
|
||||
}
|
||||
} else {
|
||||
let dbIssues = dbMap.get(element.targetName);
|
||||
if (dbIssues) {
|
||||
dbMap.set(element.targetName, dbIssues.concat([issues]));
|
||||
} else {
|
||||
dbMap.set(element.targetName, [issues]);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return dbMap;
|
||||
}
|
||||
|
||||
protected async execute() {
|
||||
this.model._migrationDbs = this._tree.selectedDbs();
|
||||
this.skuRecommendationPage.refreshDatabaseCount(this._model._migrationDbs.length);
|
||||
if (this._targetType === MigrationTargetType.SQLVM) {
|
||||
this._model._vmDbs = this._tree.selectedDbs();
|
||||
} else {
|
||||
this._model._miDbs = this._tree.selectedDbs();
|
||||
}
|
||||
this._skuRecommendationPage.refreshCardText();
|
||||
this.model.refreshDatabaseBackupPage = true;
|
||||
this._isOpen = false;
|
||||
}
|
||||
@@ -136,7 +100,6 @@ export class AssessmentResultsDialog {
|
||||
this._isOpen = false;
|
||||
}
|
||||
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export abstract class AssessmentDialogComponent {
|
||||
|
||||
abstract createComponent(view: azdata.ModelView): Promise<azdata.Component>;
|
||||
}
|
||||
@@ -3,10 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { AssessmentDialogComponent } from './model/assessmentDialogComponent';
|
||||
|
||||
export class SqlAssessmentResult extends AssessmentDialogComponent {
|
||||
export class SqlAssessmentResult {
|
||||
async createComponent(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
|
||||
const title = this.createTitleComponent(view);
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as azdata from 'azdata';
|
||||
import { AssessmentDialogComponent } from './model/assessmentDialogComponent';
|
||||
|
||||
export class SqlAssessmentResultList extends AssessmentDialogComponent {
|
||||
export class SqlAssessmentResultList {
|
||||
async createComponent(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
|
||||
return view.modelBuilder.divContainer().withItems([
|
||||
|
||||
@@ -3,49 +3,54 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as azdata from 'azdata';
|
||||
import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
|
||||
import { MigrationStateModel } from '../../models/stateMachine';
|
||||
import { Issues } from './assessmentResultsDialog';
|
||||
import { AssessmentDialogComponent } from './model/assessmentDialogComponent';
|
||||
import { SqlMigrationAssessmentResultItem, SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
|
||||
import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
|
||||
|
||||
type DbIssues = {
|
||||
name: string,
|
||||
issues: Issues[]
|
||||
const styleLeft: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden'
|
||||
};
|
||||
const styleRight: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'right',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden'
|
||||
};
|
||||
export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
|
||||
public static excludeDbs: Array<string> = ['master', 'tempdb', 'msdb', 'model'];
|
||||
private _model!: MigrationStateModel;
|
||||
private instanceTable!: azdata.ComponentBuilder<azdata.DeclarativeTableComponent, azdata.DeclarativeTableProperties>;
|
||||
private databaseTable!: azdata.ComponentBuilder<azdata.DeclarativeTableComponent, azdata.DeclarativeTableProperties>;
|
||||
private _assessmentResultsTable!: azdata.ComponentBuilder<azdata.DeclarativeTableComponent, azdata.DeclarativeTableProperties>;
|
||||
private _impactedObjectsTable!: azdata.ComponentBuilder<azdata.DeclarativeTableComponent, azdata.DeclarativeTableProperties>;
|
||||
private _assessmentData: Map<string, Issues[]>;
|
||||
export class SqlDatabaseTree {
|
||||
|
||||
private _instanceTable!: azdata.DeclarativeTableComponent;
|
||||
private _databaseTable!: azdata.DeclarativeTableComponent;
|
||||
private _assessmentResultsTable!: azdata.DeclarativeTableComponent;
|
||||
|
||||
private _impactedObjectsTable!: azdata.DeclarativeTableComponent;
|
||||
|
||||
private _recommendation!: azdata.TextComponent;
|
||||
private _dbName!: azdata.TextComponent;
|
||||
private _recommendationText!: azdata.TextComponent;
|
||||
private _descriptionText!: azdata.TextComponent;
|
||||
private _issues!: Issues;
|
||||
private _impactedObjects!: SqlMigrationImpactedObjectInfo[];
|
||||
private _objectDetailsType!: azdata.TextComponent;
|
||||
private _objectDetailsName!: azdata.TextComponent;
|
||||
private _objectDetailsSample!: azdata.TextComponent;
|
||||
private _moreInfo!: azdata.TextComponent;
|
||||
private _assessmentType!: string;
|
||||
private _moreInfo!: azdata.HyperlinkComponent;
|
||||
private _assessmentTitle!: azdata.TextComponent;
|
||||
|
||||
constructor(model: MigrationStateModel, assessmentData: Map<string, Issues[]>, assessmentType: string) {
|
||||
super();
|
||||
this._assessmentData = assessmentData;
|
||||
this._model = model;
|
||||
this._assessmentType = assessmentType;
|
||||
if (this._assessmentType === 'vm') {
|
||||
this._assessmentData.clear();
|
||||
}
|
||||
private _activeIssues!: SqlMigrationAssessmentResultItem[];
|
||||
private _selectedIssue!: SqlMigrationAssessmentResultItem;
|
||||
private _selectedObject!: SqlMigrationImpactedObjectInfo;
|
||||
|
||||
constructor(
|
||||
private _model: MigrationStateModel,
|
||||
private _targetType: MigrationTargetType
|
||||
) {
|
||||
}
|
||||
|
||||
async createComponent(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
async createComponent(view: azdata.ModelView, dbs: string[]): Promise<azdata.Component> {
|
||||
const component = view.modelBuilder.flexContainer().withLayout({
|
||||
height: '100%',
|
||||
flexFlow: 'column'
|
||||
@@ -57,32 +62,15 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
|
||||
component.addItem(this.createSearchComponent(view), { flex: '0 0 auto' });
|
||||
component.addItem(this.createInstanceComponent(view), { flex: '0 0 auto' });
|
||||
component.addItem(await this.createDatabaseComponent(view), { flex: '1 1 auto' });
|
||||
component.addItem(this.createDatabaseComponent(view, dbs), { flex: '1 1 auto' });
|
||||
return component;
|
||||
}
|
||||
|
||||
private async createDatabaseComponent(view: azdata.ModelView): Promise<azdata.DivContainer> {
|
||||
|
||||
let mapRowIssue = new Map<number, DbIssues>();
|
||||
const styleLeft: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden'
|
||||
};
|
||||
const styleRight: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'right',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden'
|
||||
};
|
||||
|
||||
this.databaseTable = view.modelBuilder.declarativeTable().withProps(
|
||||
private createDatabaseComponent(view: azdata.ModelView, dbs: string[]): azdata.DivContainer {
|
||||
this._databaseTable = view.modelBuilder.declarativeTable().withProps(
|
||||
{
|
||||
selectEffect: true,
|
||||
width: '350px',
|
||||
width: 200,
|
||||
CSSStyles: {
|
||||
'table-layout': 'fixed'
|
||||
},
|
||||
@@ -90,7 +78,7 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
{
|
||||
displayName: '',
|
||||
valueType: azdata.DeclarativeDataType.boolean,
|
||||
width: '10%',
|
||||
width: 20,
|
||||
isReadOnly: false,
|
||||
showCheckAll: true,
|
||||
headerCssStyles: styleLeft,
|
||||
@@ -99,125 +87,36 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
{
|
||||
displayName: 'Databases', // TODO localize
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '75%',
|
||||
width: 100,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleLeft
|
||||
},
|
||||
{
|
||||
displayName: 'Issues', // Incidents
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '15%',
|
||||
width: 30,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleRight,
|
||||
ariaLabel: 'Issue Count' // TODO localize
|
||||
|
||||
}
|
||||
],
|
||||
dataValues: [
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
let dbList = await azdata.connection.listDatabases(this._model.sourceConnectionId);
|
||||
|
||||
if (dbList.length > 0) {
|
||||
let rowNumber = 0;
|
||||
this._assessmentData.forEach((value, key) => {
|
||||
this.databaseTable.component().dataValues?.push(
|
||||
[
|
||||
{
|
||||
value: false,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: key,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: value.length,
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
|
||||
);
|
||||
let dbIssues = {
|
||||
name: key,
|
||||
issues: value
|
||||
};
|
||||
mapRowIssue.set(rowNumber, dbIssues);
|
||||
dbList = dbList.filter(obj => obj !== key);
|
||||
|
||||
rowNumber = rowNumber + 1;
|
||||
});
|
||||
|
||||
dbList.filter(db => !SqlDatabaseTree.excludeDbs.includes(db)).forEach((value) => {
|
||||
this.databaseTable.component().dataValues?.push(
|
||||
[
|
||||
{
|
||||
value: true,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: value,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
|
||||
);
|
||||
let impactedObjects: SqlMigrationImpactedObjectInfo[] = [];
|
||||
let issue: Issues[] = [{
|
||||
description: 'No Issues',
|
||||
recommendation: 'No Issues',
|
||||
moreInfo: 'No Issues',
|
||||
impactedObjects: impactedObjects,
|
||||
rowNumber: rowNumber
|
||||
}];
|
||||
let noIssues = {
|
||||
name: value,
|
||||
issues: issue
|
||||
};
|
||||
mapRowIssue.set(rowNumber, noIssues);
|
||||
rowNumber = rowNumber + 1;
|
||||
});
|
||||
}
|
||||
|
||||
this.databaseTable.component().onRowSelected(({ row }) => {
|
||||
const rowInfo = mapRowIssue.get(row);
|
||||
if (rowInfo) {
|
||||
this._assessmentResultsTable.component().dataValues = [];
|
||||
this._dbName.value = rowInfo.name;
|
||||
if (rowInfo.issues[0].description === 'No Issues') {
|
||||
this._recommendation.value = `Warnings (0 issues found)`;
|
||||
} else {
|
||||
this._recommendation.value = `Warnings (${rowInfo.issues.length} issues found)`;
|
||||
}
|
||||
|
||||
// Need some kind of refresh method for declarative tables
|
||||
let dataValues: string[][] = [];
|
||||
rowInfo.issues.forEach(async (issue) => {
|
||||
dataValues.push([
|
||||
issue.description
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
this._assessmentResultsTable.component().updateProperties({
|
||||
data: dataValues
|
||||
});
|
||||
|
||||
}
|
||||
).component();
|
||||
|
||||
this._databaseTable.onRowSelected(({ row }) => {
|
||||
this._databaseTable.focus();
|
||||
this._activeIssues = this._model._assessmentResults?.databaseAssessments[row].issues;
|
||||
this._selectedIssue = this._model._assessmentResults?.databaseAssessments[row].issues[0];
|
||||
this._dbName.value = <string>this._databaseTable.dataValues![row][1].value;
|
||||
this.refreshResults();
|
||||
});
|
||||
|
||||
|
||||
const tableContainer = view.modelBuilder.divContainer().withItems([this.databaseTable.component()]).withProps({
|
||||
const tableContainer = view.modelBuilder.divContainer().withItems([this._databaseTable]).withProps({
|
||||
CSSStyles: {
|
||||
'width': '200px',
|
||||
'margin-left': '15px',
|
||||
},
|
||||
'margin-right': '5px',
|
||||
'margin-bottom': '10px'
|
||||
}
|
||||
}).component();
|
||||
return tableContainer;
|
||||
}
|
||||
@@ -225,41 +124,30 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
private createSearchComponent(view: azdata.ModelView): azdata.DivContainer {
|
||||
let resourceSearchBox = view.modelBuilder.inputBox().withProperties({
|
||||
placeHolder: 'Search',
|
||||
ariaLabel: 'searchbar'
|
||||
}).component();
|
||||
|
||||
const searchContainer = view.modelBuilder.divContainer().withItems([resourceSearchBox]).withProps({
|
||||
CSSStyles: {
|
||||
'width': '200px',
|
||||
'margin-left': '15px',
|
||||
'margin-right': '5px'
|
||||
|
||||
},
|
||||
'margin-right': '5px',
|
||||
'margin-bottom': '10px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
return searchContainer;
|
||||
}
|
||||
|
||||
private createInstanceComponent(view: azdata.ModelView): azdata.DivContainer {
|
||||
const styleLeft: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'left'
|
||||
};
|
||||
|
||||
const styleRight: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'right'
|
||||
};
|
||||
|
||||
this.instanceTable = view.modelBuilder.declarativeTable().withProps(
|
||||
this._instanceTable = view.modelBuilder.declarativeTable().withProps(
|
||||
{
|
||||
selectEffect: true,
|
||||
width: '100%',
|
||||
width: 200,
|
||||
columns: [
|
||||
{
|
||||
displayName: 'Instance',
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: 5,
|
||||
width: 150,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleLeft,
|
||||
ariaLabel: 'Database Migration Check' // TODO localize
|
||||
@@ -267,31 +155,30 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
{
|
||||
displayName: 'Warnings', // TODO localize
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: 1,
|
||||
width: 50,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleRight
|
||||
}
|
||||
],
|
||||
dataValues: [
|
||||
[
|
||||
{
|
||||
value: 'SQL Server 1',
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
]
|
||||
});
|
||||
|
||||
const instanceContainer = view.modelBuilder.divContainer().withItems([this.instanceTable.component()]).withProps({
|
||||
}).component();
|
||||
|
||||
const instanceContainer = view.modelBuilder.divContainer().withItems([this._instanceTable]).withProps({
|
||||
CSSStyles: {
|
||||
'width': '200px',
|
||||
'margin-left': '15px',
|
||||
},
|
||||
'margin-right': '5px',
|
||||
'margin-bottom': '10px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._instanceTable.onRowSelected((e) => {
|
||||
this._activeIssues = this._model._assessmentResults?.issues;
|
||||
this._selectedIssue = this._model._assessmentResults?.issues[0];
|
||||
this._dbName.value = <string>this._instanceTable.dataValues![0][0].value;
|
||||
this.refreshResults();
|
||||
});
|
||||
|
||||
return instanceContainer;
|
||||
}
|
||||
|
||||
@@ -434,18 +321,12 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
]
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
this._impactedObjectsTable.component().onRowSelected(({ row }) => {
|
||||
if (this._dbName.value) {
|
||||
this._impactedObjects = this._issues.impactedObjects;
|
||||
}
|
||||
this._objectDetailsType.value = `Type: ${this._impactedObjects[row].objectType!}`;
|
||||
this._objectDetailsName.value = `Name: ${this._impactedObjects[row].name}`;
|
||||
this._objectDetailsSample.value = this._impactedObjects[row].impactDetail;
|
||||
).component();
|
||||
|
||||
|
||||
this._impactedObjectsTable.onRowSelected(({ row }) => {
|
||||
this._selectedObject = this._impactedObjects[row];
|
||||
this.refreshImpactedObject();
|
||||
});
|
||||
|
||||
|
||||
@@ -488,7 +369,7 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
}
|
||||
}).component();
|
||||
|
||||
const container = view.modelBuilder.flexContainer().withItems([impactedObjectsTitle, this._impactedObjectsTable.component(), objectDetailsTitle, this._objectDetailsType, this._objectDetailsName, this._objectDetailsSample]).withLayout({
|
||||
const container = view.modelBuilder.flexContainer().withItems([impactedObjectsTitle, this._impactedObjectsTable, objectDetailsTitle, this._objectDetailsType, this._objectDetailsName, this._objectDetailsSample]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
@@ -534,13 +415,7 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
'margin-block-end': '0px'
|
||||
}
|
||||
}).component();
|
||||
this._moreInfo = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: '',
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
'width': '250px'
|
||||
}
|
||||
}).component();
|
||||
this._moreInfo = view.modelBuilder.hyperlink().component();
|
||||
|
||||
|
||||
const container = view.modelBuilder.flexContainer().withItems([descriptionTitle, this._descriptionText, recommendationTitle, this._recommendationText, moreInfo, this._moreInfo]).withLayout({
|
||||
@@ -555,7 +430,8 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
this._assessmentTitle = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: '',
|
||||
CSSStyles: {
|
||||
'font-size': '14px',
|
||||
'font-size': '15px',
|
||||
'line-size': '19px',
|
||||
'padding-bottom': '15px',
|
||||
'border-bottom': 'solid 1px'
|
||||
}
|
||||
@@ -568,7 +444,8 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
const title = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: 'Target Platform',
|
||||
CSSStyles: {
|
||||
'font-size': '14px',
|
||||
'font-size': '13px',
|
||||
'line-size': '19px',
|
||||
'margin-block-start': '0px',
|
||||
'margin-block-end': '2px'
|
||||
}
|
||||
@@ -580,7 +457,7 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
private createPlatformComponent(view: azdata.ModelView): azdata.TextComponent {
|
||||
const impact = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
title: 'Platform', // TODO localize
|
||||
value: 'Azure SQL Managed Instance',
|
||||
value: (this._targetType === MigrationTargetType.SQLVM) ? 'Azure SQL Virtual Machine' : 'Azure SQL Managed Instance',
|
||||
CSSStyles: {
|
||||
'font-size': '18px',
|
||||
'margin-block-start': '0px',
|
||||
@@ -666,49 +543,162 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
|
||||
]
|
||||
]
|
||||
}
|
||||
);
|
||||
).component();
|
||||
|
||||
this._assessmentResultsTable.component().onRowSelected(({ row }) => {
|
||||
this._descriptionText.value = this._assessmentResultsTable.component().data![row][0];
|
||||
|
||||
if (this._dbName.value) {
|
||||
this._issues = this._assessmentData.get(this._dbName.value)![row];
|
||||
this._moreInfo.value = this._issues.moreInfo;
|
||||
this._impactedObjects = this._issues.impactedObjects;
|
||||
let data: { value: string; }[][] = [];
|
||||
this._impactedObjects.forEach(async (impactedObject) => {
|
||||
data.push([
|
||||
{
|
||||
value: impactedObject.objectType
|
||||
},
|
||||
{
|
||||
value: impactedObject.name
|
||||
}
|
||||
|
||||
]);
|
||||
});
|
||||
|
||||
this._assessmentTitle.value = this._issues.description;
|
||||
|
||||
this._impactedObjectsTable.component().updateProperties({
|
||||
dataValues: data
|
||||
});
|
||||
}
|
||||
this._assessmentResultsTable.onRowSelected(({ row }) => {
|
||||
this._selectedIssue = this._activeIssues[row];
|
||||
this.refreshAssessmentDetails();
|
||||
});
|
||||
|
||||
return this._assessmentResultsTable.component();
|
||||
return this._assessmentResultsTable;
|
||||
}
|
||||
|
||||
public selectedDbs(): string[] {
|
||||
let result: string[] = [];
|
||||
|
||||
this.databaseTable.component().dataValues?.forEach((arr) => {
|
||||
this._databaseTable.dataValues?.forEach((arr) => {
|
||||
if (arr[0].value === true) {
|
||||
result.push(arr[1].value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public refreshResults(): void {
|
||||
const assessmentResults: azdata.DeclarativeTableCellValue[][] = [];
|
||||
this._activeIssues.forEach((v) => {
|
||||
assessmentResults.push(
|
||||
[
|
||||
{
|
||||
value: v.checkId
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
this._assessmentResultsTable.dataValues = assessmentResults;
|
||||
this._selectedIssue = this._activeIssues[0];
|
||||
this.refreshAssessmentDetails();
|
||||
}
|
||||
|
||||
public refreshAssessmentDetails(): void {
|
||||
if (this._selectedIssue) {
|
||||
this._assessmentTitle.value = this._selectedIssue.checkId;
|
||||
this._descriptionText.value = this._selectedIssue.description;
|
||||
this._moreInfo.url = this._selectedIssue.helpLink;
|
||||
this._moreInfo.label = this._selectedIssue.helpLink;
|
||||
this._impactedObjects = this._selectedIssue.impactedObjects;
|
||||
this._recommendationText.value = this._selectedIssue.message; // Expose correct property for recommendation.
|
||||
this._impactedObjectsTable.dataValues = this._selectedIssue.impactedObjects.map((object) => {
|
||||
return [
|
||||
{
|
||||
value: object.objectType
|
||||
},
|
||||
{
|
||||
value: object.name
|
||||
}
|
||||
];
|
||||
});
|
||||
this._selectedObject = this._selectedIssue.impactedObjects[0];
|
||||
}
|
||||
else {
|
||||
this._assessmentTitle.value = '';
|
||||
this._descriptionText.value = '';
|
||||
this._moreInfo.url = '';
|
||||
this._moreInfo.label = '';
|
||||
this._recommendationText.value = '';
|
||||
this._impactedObjectsTable.dataValues = [];
|
||||
}
|
||||
this.refreshImpactedObject();
|
||||
}
|
||||
|
||||
public refreshImpactedObject(): void {
|
||||
if (this._selectedObject) {
|
||||
this._objectDetailsType.value = `Type: ${this._selectedObject.objectType!}`;
|
||||
this._objectDetailsName.value = `Name: ${this._selectedObject.name}`;
|
||||
this._objectDetailsSample.value = this._selectedObject.impactDetail;
|
||||
} else {
|
||||
this._objectDetailsType.value = ``;
|
||||
this._objectDetailsName.value = ``;
|
||||
this._objectDetailsSample.value = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
let instanceTableValues: azdata.DeclarativeTableCellValue[][] = [];
|
||||
let databaseTableValues: azdata.DeclarativeTableCellValue[][] = [];
|
||||
const excludedDatabases = ['master', 'msdb', 'tempdb', 'model'];
|
||||
const dbList = (await azdata.connection.listDatabases(this._model.sourceConnectionId)).filter(db => !excludedDatabases.includes(db));
|
||||
const selectedDbs = (this._targetType === MigrationTargetType.SQLVM) ? this._model._vmDbs : this._model._miDbs;
|
||||
const serverName = (await this._model.getSourceConnectionProfile()).serverName;
|
||||
|
||||
if (this._targetType === MigrationTargetType.SQLVM || !this._model._assessmentResults) {
|
||||
instanceTableValues = [
|
||||
[
|
||||
{
|
||||
value: serverName,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
];
|
||||
dbList.forEach((db) => {
|
||||
databaseTableValues.push(
|
||||
[
|
||||
{
|
||||
value: selectedDbs.includes(db),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: db,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
} else {
|
||||
instanceTableValues = [
|
||||
[
|
||||
{
|
||||
value: serverName,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: this._model._assessmentResults.issues.length,
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
];
|
||||
this._model._assessmentResults.databaseAssessments.forEach((db) => {
|
||||
databaseTableValues.push(
|
||||
[
|
||||
{
|
||||
value: selectedDbs.includes(db.name),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: db.name,
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: db.issues.length,
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
this._dbName.value = serverName;
|
||||
this._activeIssues = this._model._assessmentResults.issues;
|
||||
this._selectedIssue = this._model._assessmentResults?.issues[0];
|
||||
this.refreshResults();
|
||||
this._instanceTable.dataValues = instanceTableValues;
|
||||
this._databaseTable.dataValues = databaseTableValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
||||
import * as loc from '../../constants/strings';
|
||||
import { getSqlServerName } from '../../api/utils';
|
||||
import { EOL } from 'os';
|
||||
export class MigrationCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -353,6 +354,13 @@ export class MigrationCutoverDialog {
|
||||
this._cutoverButton.enabled = false;
|
||||
this._cancelButton.enabled = false;
|
||||
await this._model.fetchStatus();
|
||||
const errors = [];
|
||||
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
|
||||
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
|
||||
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.restoreBlockingReason);
|
||||
this._dialogObject.message = {
|
||||
text: errors.filter(e => e !== undefined).join(EOL)
|
||||
};
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId);
|
||||
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
|
||||
@@ -20,6 +20,8 @@ export class MigrationCutoverDialogModel {
|
||||
this._migration.subscription,
|
||||
this._migration.migrationContext
|
||||
));
|
||||
// Logging status to help debugging.
|
||||
console.log(this.migrationStatus);
|
||||
}
|
||||
|
||||
public async startCutover(): Promise<DatabaseMigration | undefined> {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { SKURecommendation } from './product';
|
||||
import { MigrationTargetType } from './stateMachine';
|
||||
|
||||
export interface Base {
|
||||
uuid: string;
|
||||
@@ -21,6 +21,15 @@ export interface GatherInformationRequest extends BaseRequest {
|
||||
connection: azdata.connection.Connection;
|
||||
}
|
||||
|
||||
export interface Checks {
|
||||
|
||||
}
|
||||
|
||||
export interface SKURecommendation {
|
||||
product: MigrationTargetType;
|
||||
checks: Checks;
|
||||
}
|
||||
|
||||
export interface SKURecommendations {
|
||||
recommendations: SKURecommendation[];
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { IconPath } from 'azdata';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export type MigrationProductType = 'AzureSQLMI' | 'AzureSQLVM';
|
||||
export interface MigrationProduct {
|
||||
readonly type: MigrationProductType;
|
||||
}
|
||||
|
||||
export interface Check {
|
||||
|
||||
}
|
||||
|
||||
export interface Checks {
|
||||
// fill some information
|
||||
checks: Check;
|
||||
// If there is not going to be any more information, use Check[] directly
|
||||
}
|
||||
|
||||
export interface Product extends MigrationProduct {
|
||||
readonly name: string;
|
||||
readonly learnMoreLink?: string;
|
||||
readonly icon?: IconPath;
|
||||
}
|
||||
|
||||
export class Product implements Product {
|
||||
constructor(public readonly type: MigrationProductType, public readonly name: string, public readonly icon?: IconPath, public readonly learnMoreLink?: string) {
|
||||
|
||||
}
|
||||
|
||||
static FromMigrationProduct(migrationProduct: MigrationProduct) {
|
||||
// TODO: populatie from some lookup table;
|
||||
|
||||
const product: Product | undefined = ProductLookupTable[migrationProduct.type];
|
||||
return new Product(migrationProduct.type, product?.name ?? '', product.icon ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
export interface SKURecommendation {
|
||||
product: MigrationProduct;
|
||||
checks: Checks;
|
||||
}
|
||||
|
||||
|
||||
export const ProductLookupTable: { [key in MigrationProductType]: Product } = {
|
||||
'AzureSQLMI': {
|
||||
type: 'AzureSQLMI',
|
||||
name: localize('sql.migration.products.azuresqlmi.name', 'Azure Managed Instance (Microsoft managed)'),
|
||||
icon: 'sqlMI.svg'
|
||||
},
|
||||
'AzureSQLVM': {
|
||||
type: 'AzureSQLVM',
|
||||
name: localize('sql.migration.products.azuresqlvm.name', 'Azure SQL Virtual Machine (Customer managed)'),
|
||||
icon: 'sqlVM.svg'
|
||||
}
|
||||
};
|
||||
@@ -33,6 +33,11 @@ export enum State {
|
||||
EXIT,
|
||||
}
|
||||
|
||||
export enum MigrationTargetType {
|
||||
SQLVM = 'sqlvm',
|
||||
SQLMI = 'sqlmi'
|
||||
}
|
||||
|
||||
export enum MigrationCutover {
|
||||
ONLINE,
|
||||
OFFLINE
|
||||
@@ -82,6 +87,11 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _azureAccount!: azdata.Account;
|
||||
public _accountTenants!: azurecore.Tenant[];
|
||||
|
||||
public _connecionProfile!: azdata.connection.ConnectionProfile;
|
||||
public _authenticationType!: string;
|
||||
public _sqlServerUsername!: string;
|
||||
public _sqlServerPassword!: string;
|
||||
|
||||
public _subscriptions!: azureResource.AzureResourceSubscription[];
|
||||
|
||||
public _targetSubscription!: azureResource.AzureResourceSubscription;
|
||||
@@ -103,9 +113,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
|
||||
private _currentState: State;
|
||||
private _gatheringInformationError: string | undefined;
|
||||
private _skuRecommendations: SKURecommendations | undefined;
|
||||
private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined;
|
||||
|
||||
private _skuRecommendations: SKURecommendations | undefined;
|
||||
public _assessmentResults!: ServerAssessement;
|
||||
public _vmDbs: string[] = [];
|
||||
public _miDbs: string[] = [];
|
||||
public _targetType!: MigrationTargetType;
|
||||
public refreshDatabaseBackupPage!: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -127,18 +140,52 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
|
||||
public set currentState(newState: State) {
|
||||
const oldState = this.currentState;
|
||||
|
||||
this._currentState = newState;
|
||||
|
||||
this._stateChangeEventEmitter.fire({ oldState, newState: this.currentState });
|
||||
}
|
||||
|
||||
public get assessmentResults(): mssql.SqlMigrationAssessmentResultItem[] | undefined {
|
||||
public async getServerAssessments(): Promise<ServerAssessement> {
|
||||
const excludeDbs: string[] = [
|
||||
'master',
|
||||
'tempdb',
|
||||
'msdb',
|
||||
'model'
|
||||
];
|
||||
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId);
|
||||
|
||||
const assessmentResults = await this.migrationService.getAssessments(
|
||||
ownerUri
|
||||
);
|
||||
|
||||
const serverDatabases = await (await azdata.connection.listDatabases(this.sourceConnectionId)).filter((name) => !excludeDbs.includes(name));
|
||||
const serverLevelAssessments: mssql.SqlMigrationAssessmentResultItem[] = [];
|
||||
const databaseLevelAssessments = serverDatabases.map(db => {
|
||||
return {
|
||||
name: db,
|
||||
issues: <mssql.SqlMigrationAssessmentResultItem[]>[]
|
||||
};
|
||||
});
|
||||
|
||||
assessmentResults?.items.forEach((item) => {
|
||||
const dbIndex = serverDatabases.indexOf(item.databaseName);
|
||||
if (dbIndex === -1) {
|
||||
serverLevelAssessments.push(item);
|
||||
} else {
|
||||
databaseLevelAssessments[dbIndex].issues.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
this._assessmentResults = {
|
||||
issues: serverLevelAssessments,
|
||||
databaseAssessments: databaseLevelAssessments
|
||||
};
|
||||
|
||||
return this._assessmentResults;
|
||||
}
|
||||
|
||||
public set assessmentResults(assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined) {
|
||||
this._assessmentResults = assessmentResults;
|
||||
public getDatabaseAssessments(databaseName: string): mssql.SqlMigrationAssessmentResultItem[] | undefined {
|
||||
return this._assessmentResults.databaseAssessments.find(databaseAsssessment => databaseAsssessment.name === databaseName)?.issues;
|
||||
}
|
||||
|
||||
public get gatheringInformationError(): string | undefined {
|
||||
@@ -212,6 +259,17 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return this._accountTenants[index];
|
||||
}
|
||||
|
||||
public async getSourceConnectionProfile(): Promise<azdata.connection.ConnectionProfile> {
|
||||
const sqlConnections = await azdata.connection.getConnections();
|
||||
return sqlConnections.find((value) => {
|
||||
if (value.connectionId === this.sourceConnectionId) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})!;
|
||||
}
|
||||
|
||||
public async getSubscriptionsDropdownValues(): Promise<azdata.CategoryValue[]> {
|
||||
let subscriptionsValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
@@ -289,7 +347,9 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
let virtualMachineValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
this._targetSqlVirtualMachines = await getAvailableSqlVMs(this._azureAccount, subscription);
|
||||
virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => {
|
||||
virtualMachineValues = this._targetSqlVirtualMachines.filter((virtualMachine) => {
|
||||
return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms.
|
||||
}).map((virtualMachine) => {
|
||||
return {
|
||||
name: virtualMachine.id,
|
||||
displayName: `${virtualMachine.name}`
|
||||
@@ -470,7 +530,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const connectionPassword = await azdata.connection.getCredentials(this.sourceConnectionId);
|
||||
|
||||
const requestBody: StartDatabaseMigrationRequest = {
|
||||
location: this._sqlMigrationService?.properties.location!,
|
||||
@@ -492,8 +551,9 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
},
|
||||
sourceSqlConnection: {
|
||||
dataSource: currentConnection?.serverName!,
|
||||
username: currentConnection?.userName!,
|
||||
password: connectionPassword.password
|
||||
authentication: this._authenticationType,
|
||||
username: this._sqlServerUsername,
|
||||
password: this._sqlServerPassword
|
||||
},
|
||||
scope: this._targetServerInstance.id
|
||||
}
|
||||
@@ -531,3 +591,11 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerAssessement {
|
||||
issues: mssql.SqlMigrationAssessmentResultItem[];
|
||||
databaseAssessments: {
|
||||
name: string;
|
||||
issues: mssql.SqlMigrationAssessmentResultItem[];
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -19,19 +19,19 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
private _windowsUserAccountText!: azdata.InputBoxComponent;
|
||||
private _passwordText!: azdata.InputBoxComponent;
|
||||
private _networkShareDatabaseConfigContainer!: azdata.FlexContainer;
|
||||
private _networkShareLocations!: azdata.InputBoxComponent[];
|
||||
private _networkShareLocations: azdata.InputBoxComponent[] = [];
|
||||
|
||||
private _blobContainer!: azdata.FlexContainer;
|
||||
private _blobContainerSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent;
|
||||
private _blobContainerDatabaseConfigContainer!: azdata.FlexContainer;
|
||||
private _blobContainerDropdowns!: azdata.DropDownComponent[];
|
||||
private _blobContainerDropdowns: azdata.DropDownComponent[] = [];
|
||||
|
||||
private _fileShareContainer!: azdata.FlexContainer;
|
||||
private _fileShareSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _fileShareStorageAccountDropdown!: azdata.DropDownComponent;
|
||||
private _fileShareDatabaseConfigContainer!: azdata.FlexContainer;
|
||||
private _fileShareDropdowns!: azdata.DropDownComponent[];
|
||||
private _fileShareDropdowns: azdata.DropDownComponent[] = [];
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel);
|
||||
|
||||
@@ -26,8 +26,14 @@ export class MigrationModePage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((e) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
public async onPageLeave(): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((e) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
@@ -5,53 +5,50 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import { Product } from '../models/product';
|
||||
import { MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../models/stateMachine';
|
||||
import { AssessmentResultsDialog } from '../dialog/assessmentResults/assessmentResultsDialog';
|
||||
import * as constants from '../constants/strings';
|
||||
import * as vscode from 'vscode';
|
||||
import { EOL } from 'os';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
|
||||
// import { SqlMigrationService } from '../../../../extensions/mssql/src/sqlMigration/sqlMigrationService';
|
||||
export interface Product {
|
||||
type: MigrationTargetType;
|
||||
name: string,
|
||||
icon: IconPath;
|
||||
}
|
||||
|
||||
export class SKURecommendationPage extends MigrationWizardPage {
|
||||
|
||||
private supportedProducts: Product[] = [
|
||||
private _view!: azdata.ModelView;
|
||||
private _igComponent!: azdata.TextComponent;
|
||||
private _detailsComponent!: azdata.TextComponent;
|
||||
private _chooseTargetComponent!: azdata.DivContainer;
|
||||
private _azureSubscriptionText!: azdata.TextComponent;
|
||||
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _resourceDropdownLabel!: azdata.TextComponent;
|
||||
private _resourceDropdown!: azdata.DropDownComponent;
|
||||
private _rbg!: azdata.RadioCardGroupComponent;
|
||||
private eventListener!: vscode.Disposable;
|
||||
|
||||
private _supportedProducts: Product[] = [
|
||||
{
|
||||
type: 'AzureSQLMI',
|
||||
type: MigrationTargetType.SQLMI,
|
||||
name: constants.SKU_RECOMMENDATION_MI_CARD_TEXT,
|
||||
icon: IconPathHelper.sqlMiLogo
|
||||
},
|
||||
{
|
||||
type: 'AzureSQLVM',
|
||||
type: MigrationTargetType.SQLVM,
|
||||
name: constants.SKU_RECOMMENDATION_VM_CARD_TEXT,
|
||||
icon: IconPathHelper.sqlVmLogo
|
||||
}
|
||||
];
|
||||
|
||||
// For future reference: DO NOT EXPOSE WIZARD DIRECTLY THROUGH HERE.
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.SKU_RECOMMENDATION_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView) {
|
||||
await this.initialState(view);
|
||||
}
|
||||
|
||||
private _igComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
|
||||
private _detailsComponent: azdata.FormComponent<azdata.TextComponent> | undefined;
|
||||
private _chooseTargetComponent: azdata.FormComponent<azdata.DivContainer> | undefined;
|
||||
private _azureSubscriptionText: azdata.FormComponent<azdata.TextComponent> | undefined;
|
||||
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _resourceDropdownLabel!: azdata.TextComponent;
|
||||
private _resourceDropdown!: azdata.DropDownComponent;
|
||||
private _view: azdata.ModelView | undefined;
|
||||
private _rbg!: azdata.RadioCardGroupComponent;
|
||||
private _dbCount!: number;
|
||||
private _serverName!: string;
|
||||
|
||||
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
|
||||
@@ -81,12 +78,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
e.selected !== constants.NO_MANAGED_INSTANCE_FOUND &&
|
||||
e.selected !== constants.NO_VIRTUAL_MACHINE_FOUND) {
|
||||
this.migrationStateModel._sqlMigrationServices = undefined!;
|
||||
if (this._rbg.selectedCardId === 'AzureSQLVM') {
|
||||
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) {
|
||||
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(e.index);
|
||||
} else {
|
||||
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -101,100 +97,51 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
let connectionUri: string = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
|
||||
this.migrationStateModel.migrationService.getAssessments(connectionUri).then(results => {
|
||||
if (results) {
|
||||
this.migrationStateModel.assessmentResults = results.items;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this._view = view;
|
||||
const formContainer = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
this._igComponent,
|
||||
this._detailsComponent,
|
||||
this._chooseTargetComponent,
|
||||
this._azureSubscriptionText,
|
||||
{
|
||||
title: '',
|
||||
component: this._igComponent
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
component: this._detailsComponent
|
||||
},
|
||||
{
|
||||
title: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
|
||||
component: this._chooseTargetComponent
|
||||
},
|
||||
{
|
||||
component: this._azureSubscriptionText
|
||||
},
|
||||
{
|
||||
component: targetContainer
|
||||
},
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let data = connectionUri.split('|');
|
||||
data.forEach(element => {
|
||||
if (element.startsWith('server:')) {
|
||||
let serverArray = element.split(':');
|
||||
this._serverName = serverArray[1];
|
||||
}
|
||||
});
|
||||
this._dbCount = (await azdata.connection.listDatabases(this.migrationStateModel.sourceConnectionId)).length;
|
||||
|
||||
await view.initializeModel(formContainer.component());
|
||||
}
|
||||
|
||||
private createStatusComponent(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
|
||||
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: '',
|
||||
private createStatusComponent(view: azdata.ModelView): azdata.TextComponent {
|
||||
const component = view.modelBuilder.text().withProps({
|
||||
CSSStyles: {
|
||||
'font-size': '14px'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: component.component(),
|
||||
};
|
||||
}).component();
|
||||
return component;
|
||||
}
|
||||
|
||||
private createDetailsComponent(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
|
||||
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: '',
|
||||
});
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: component.component(),
|
||||
};
|
||||
private createDetailsComponent(view: azdata.ModelView): azdata.TextComponent {
|
||||
const component = view.modelBuilder.text().component();
|
||||
return component;
|
||||
}
|
||||
|
||||
private createChooseTargetComponent(view: azdata.ModelView) {
|
||||
const component = view.modelBuilder.divContainer();
|
||||
private createChooseTargetComponent(view: azdata.ModelView): azdata.DivContainer {
|
||||
|
||||
return {
|
||||
title: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
|
||||
component: component.component()
|
||||
};
|
||||
}
|
||||
|
||||
private constructDetails(): void {
|
||||
this._chooseTargetComponent?.component.clearItems();
|
||||
this._igComponent!.component.value = constants.ASSESSMENT_COMPLETED(this._serverName);
|
||||
if (this.migrationStateModel.assessmentResults) {
|
||||
let dbIssueCount = 0;
|
||||
let last = '';
|
||||
this.migrationStateModel.assessmentResults.forEach(element => {
|
||||
if (element.targetName !== this._serverName && element.targetName !== last) {
|
||||
dbIssueCount += 1;
|
||||
last = element.targetName;
|
||||
}
|
||||
});
|
||||
if (dbIssueCount === this._dbCount) {
|
||||
this._detailsComponent!.component.value = constants.SKU_RECOMMENDATION_NONE_SUCCESSFUL;
|
||||
} else if (dbIssueCount > 0) {
|
||||
|
||||
this._detailsComponent!.component.value = constants.SKU_RECOMMENDATION_SOME_SUCCESSFUL(this._dbCount - dbIssueCount, this._dbCount);
|
||||
} else {
|
||||
this._detailsComponent!.component.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this._dbCount);
|
||||
}
|
||||
}
|
||||
this.constructTargets();
|
||||
}
|
||||
|
||||
private constructTargets(): void {
|
||||
const products: Product[] = this.supportedProducts;
|
||||
|
||||
this._rbg = this._view!.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
|
||||
this._rbg = this._view!.modelBuilder.radioCardGroup().withProps({
|
||||
cards: [],
|
||||
cardWidth: '600px',
|
||||
cardHeight: '40px',
|
||||
@@ -203,13 +150,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
iconWidth: '30px'
|
||||
}).component();
|
||||
|
||||
products.forEach((product) => {
|
||||
let dbCount = 0;
|
||||
if (product.type === 'AzureSQLVM') {
|
||||
dbCount = this._dbCount;
|
||||
} else {
|
||||
dbCount = this.migrationStateModel._migrationDbs.length;
|
||||
}
|
||||
this._supportedProducts.forEach((product) => {
|
||||
const descriptions: azdata.RadioCardDescription[] = [
|
||||
{
|
||||
textValue: product.name,
|
||||
@@ -230,7 +171,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
},
|
||||
},
|
||||
{
|
||||
textValue: `${dbCount} databases will be migrated`,
|
||||
textValue: '0 selected',
|
||||
textStyles: {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px'
|
||||
@@ -254,49 +195,73 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
descriptions
|
||||
});
|
||||
});
|
||||
let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog', this, 'mi');
|
||||
let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog', this, 'vm');
|
||||
let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE, this, MigrationTargetType.SQLMI);
|
||||
let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE, this, MigrationTargetType.SQLVM);
|
||||
|
||||
this._rbg.onLinkClick(async (value) => {
|
||||
|
||||
//check which card is being selected, and open correct dialog based on link
|
||||
if (value.description.linkDisplayValue === 'View/Change') {
|
||||
if (value.cardId === 'AzureSQLVM') {
|
||||
if (value.cardId === MigrationTargetType.SQLVM) {
|
||||
this._rbg.selectedCardId = MigrationTargetType.SQLVM;
|
||||
if (value.description.linkDisplayValue === 'View/Change') {
|
||||
await vmDialog.openDialog();
|
||||
} else if (value.cardId === 'AzureSQLMI') {
|
||||
await miDialog.openDialog();
|
||||
} else if (value.description.linkDisplayValue === 'Learn more') {
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/azure/azure-sql/virtual-machines/windows/sql-server-on-azure-vm-iaas-what-is-overview'));
|
||||
}
|
||||
} else if (value.description.linkDisplayValue === 'Learn more') {
|
||||
if (value.cardId === 'AzureSQLVM') {
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/en-us/azure/azure-sql/virtual-machines/windows/sql-server-on-azure-vm-iaas-what-is-overview'));
|
||||
} else if (value.cardId === 'AzureSQLMI') {
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/en-us/azure/azure-sql/managed-instance/sql-managed-instance-paas-overview '));
|
||||
} else if (value.cardId === MigrationTargetType.SQLMI) {
|
||||
this._rbg.selectedCardId = MigrationTargetType.SQLMI;
|
||||
if (value.description.linkDisplayValue === 'View/Change') {
|
||||
await miDialog.openDialog();
|
||||
} else if (value.description.linkDisplayValue === 'Learn more') {
|
||||
vscode.env.openExternal(vscode.Uri.parse('https://docs.microsoft.com/azure/azure-sql/managed-instance/sql-managed-instance-paas-overview '));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._rbg.onSelectionChanged((value) => {
|
||||
this.populateResourceInstanceDropdown();
|
||||
this.changeTargetType(value.cardId);
|
||||
});
|
||||
|
||||
this._rbg.selectedCardId = 'AzureSQLMI';
|
||||
this._rbg.selectedCardId = MigrationTargetType.SQLMI;
|
||||
|
||||
this._chooseTargetComponent?.component.addItem(this._rbg);
|
||||
|
||||
const component = view.modelBuilder.divContainer().withItems(
|
||||
[
|
||||
this._rbg
|
||||
]
|
||||
).component();
|
||||
return component;
|
||||
}
|
||||
|
||||
private createAzureSubscriptionText(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
|
||||
const component = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: 'Select an Azure subscription and an Azure SQL Managed Instance for your target.', //TODO: Localize
|
||||
private changeTargetType(newTargetType: string) {
|
||||
if (newTargetType === MigrationTargetType.SQLMI) {
|
||||
this._azureSubscriptionText.value = constants.SELECT_AZURE_MI;
|
||||
this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs;
|
||||
} else {
|
||||
this._azureSubscriptionText.value = constants.SELECT_AZURE_VM;
|
||||
this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs;
|
||||
}
|
||||
this.migrationStateModel.refreshDatabaseBackupPage = true;
|
||||
this.populateResourceInstanceDropdown();
|
||||
}
|
||||
|
||||
private async constructDetails(): Promise<void> {
|
||||
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
|
||||
this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName);
|
||||
await this.migrationStateModel.getServerAssessments();
|
||||
if (this.migrationStateModel._assessmentResults) {
|
||||
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults.databaseAssessments.length);
|
||||
}
|
||||
this.refreshCardText();
|
||||
}
|
||||
|
||||
private createAzureSubscriptionText(view: azdata.ModelView): azdata.TextComponent {
|
||||
const component = view.modelBuilder.text().withProps({
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px'
|
||||
}
|
||||
});
|
||||
}).component();
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: component.component(),
|
||||
};
|
||||
return component;
|
||||
}
|
||||
|
||||
private async populateSubscriptionDropdown(): Promise<void> {
|
||||
@@ -316,7 +281,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
private async populateResourceInstanceDropdown(): Promise<void> {
|
||||
this._resourceDropdown.loading = true;
|
||||
try {
|
||||
if (this._rbg.selectedCardId === 'AzureSQLVM') {
|
||||
if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) {
|
||||
this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
||||
this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription);
|
||||
|
||||
@@ -331,12 +296,17 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}
|
||||
}
|
||||
|
||||
private eventListener: vscode.Disposable | undefined;
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e));
|
||||
this.populateSubscriptionDropdown();
|
||||
this.constructDetails();
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
try {
|
||||
this.migrationStateModel.getServerAssessments().then((result) => {
|
||||
this.constructDetails();
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
this.populateSubscriptionDropdown();
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
const errors: string[] = [];
|
||||
this.wizard.message = {
|
||||
@@ -384,23 +354,53 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
switch (e.newState) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public refreshDatabaseCount(count: number): void {
|
||||
public refreshDatabaseCount(selectedDbs: string[]): void {
|
||||
this.migrationStateModel._migrationDbs = selectedDbs;
|
||||
this.refreshCardText();
|
||||
}
|
||||
|
||||
public refreshCardText(): 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.cards[1].descriptions[1].textValue = textValue;
|
||||
|
||||
this._rbg.updateProperties({
|
||||
cards: this._rbg.cards
|
||||
});
|
||||
if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) {
|
||||
this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs;
|
||||
} else {
|
||||
this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs;
|
||||
}
|
||||
|
||||
|
||||
if (this.migrationStateModel._assessmentResults) {
|
||||
const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments.length;
|
||||
|
||||
const dbWithIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length > 0).length;
|
||||
const miCardText = `${dbWithIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`;
|
||||
this._rbg.cards[0].descriptions[1].textValue = miCardText;
|
||||
|
||||
const vmCardText = `${dbCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._vmDbs.length} selected)`;
|
||||
this._rbg.cards[1].descriptions[1].textValue = vmCardText;
|
||||
|
||||
this._rbg.updateProperties({
|
||||
cards: this._rbg.cards
|
||||
});
|
||||
} else {
|
||||
|
||||
const miCardText = `${this.migrationStateModel._miDbs.length} selected`;
|
||||
this._rbg.cards[0].descriptions[1].textValue = miCardText;
|
||||
|
||||
const vmCardText = `${this.migrationStateModel._vmDbs.length} selected`;
|
||||
this._rbg.cards[1].descriptions[1].textValue = vmCardText;
|
||||
|
||||
this._rbg.updateProperties({
|
||||
cards: this._rbg.cards
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
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';
|
||||
|
||||
export class SourceConfigurationPage extends MigrationWizardPage {
|
||||
// For future reference: DO NOT EXPOSE WIZARD DIRECTLY THROUGH HERE.
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(SOURCE_CONFIGURATION_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView) {
|
||||
await this.initialState(view);
|
||||
}
|
||||
|
||||
private gatheringInfoComponent!: azdata.FormComponent;
|
||||
private async initialState(view: azdata.ModelView) {
|
||||
this.gatheringInfoComponent = this.createGatheringInfoComponent(view);
|
||||
const form = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
this.gatheringInfoComponent
|
||||
],
|
||||
{
|
||||
titleFontSize: '20px'
|
||||
}
|
||||
).component();
|
||||
|
||||
await view.initializeModel(form);
|
||||
|
||||
let connectionUri: string = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
|
||||
this.migrationStateModel.migrationService.getAssessments(connectionUri).then(results => {
|
||||
if (results) {
|
||||
this.migrationStateModel.assessmentResults = results.items;
|
||||
this.migrationStateModel.currentState = State.TARGET_SELECTION;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async enterErrorState() {
|
||||
const component = this.gatheringInfoComponent.component as azdata.TextComponent;
|
||||
component.value = COLLECTING_SOURCE_CONFIGURATIONS_ERROR(this.migrationStateModel.gatheringInformationError);
|
||||
}
|
||||
|
||||
private async enterTargetSelectionState() {
|
||||
this.goToNextPage();
|
||||
}
|
||||
|
||||
//#region component builders
|
||||
private createGatheringInfoComponent(view: azdata.ModelView): azdata.FormComponent {
|
||||
let explaination = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: COLLECTING_SOURCE_CONFIGURATIONS_INFO,
|
||||
CSSStyles: {
|
||||
'font-size': '14px'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
component: explaination.component(),
|
||||
title: COLLECTING_SOURCE_CONFIGURATIONS
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
private eventListener: Disposable | undefined;
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e));
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
this.eventListener?.dispose();
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
switch (e.newState) {
|
||||
case State.COLLECTION_SOURCE_INFO_ERROR:
|
||||
return this.enterErrorState();
|
||||
case State.TARGET_SELECTION:
|
||||
return this.enterTargetSelectionState();
|
||||
}
|
||||
}
|
||||
|
||||
public async canLeave(): Promise<boolean> {
|
||||
return this.migrationStateModel.currentState === State.TARGET_SELECTION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 os from 'os';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import { createLabelTextComponent, createHeadingTextComponent } from './wizardController';
|
||||
|
||||
export class SqlSourceConfigurationPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.SOURCE_CONFIGURATION, 'MigrationModePage'), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
this._view = view;
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
await this.createSourceCredentialContainer(),
|
||||
]
|
||||
);
|
||||
await view.initializeModel(form.component());
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
public async onPageLeave(): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private async createSourceCredentialContainer(): Promise<azdata.FormComponent> {
|
||||
|
||||
const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
|
||||
|
||||
let username;
|
||||
switch (connectionProfile.authenticationType) {
|
||||
case 'SqlLogin':
|
||||
username = connectionProfile.userName;
|
||||
this.migrationStateModel._authenticationType = 'SqlAuthentication';
|
||||
break;
|
||||
case 'Integrated':
|
||||
username = os.userInfo().username;
|
||||
this.migrationStateModel._authenticationType = 'WindowsAuthentication';
|
||||
break;
|
||||
default:
|
||||
username = '';
|
||||
}
|
||||
|
||||
const sourceCredText = createHeadingTextComponent(this._view, constants.SOURCE_CREDENTIALS);
|
||||
|
||||
const enterYourCredText = createLabelTextComponent(
|
||||
this._view,
|
||||
constants.ENTER_YOUR_SQL_CREDS(connectionProfile.serverName),
|
||||
{
|
||||
'width': '400px'
|
||||
}
|
||||
);
|
||||
|
||||
const usernameLable = this._view.modelBuilder.text().withProps({
|
||||
value: constants.USERNAME,
|
||||
requiredIndicator: true
|
||||
}).component();
|
||||
const usernameInput = this._view.modelBuilder.inputBox().withProps({
|
||||
value: username,
|
||||
required: true
|
||||
}).component();
|
||||
usernameInput.onTextChanged(value => {
|
||||
this.migrationStateModel._sqlServerUsername = value;
|
||||
});
|
||||
|
||||
const passwordLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL,
|
||||
requiredIndicator: true
|
||||
}).component();
|
||||
const passwordInput = this._view.modelBuilder.inputBox().withProps({
|
||||
value: (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password,
|
||||
required: true,
|
||||
inputType: 'password'
|
||||
}).component();
|
||||
passwordInput.onTextChanged(value => {
|
||||
this.migrationStateModel._sqlServerPassword = value;
|
||||
});
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
sourceCredText,
|
||||
enterYourCredText,
|
||||
usernameLable,
|
||||
usernameInput,
|
||||
passwordLabel,
|
||||
passwordInput
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
return {
|
||||
component: container
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { AccountsSelectionPage } from './accountsSelectionPage';
|
||||
import { IntergrationRuntimePage } from './integrationRuntimePage';
|
||||
import { SummaryPage } from './summaryPage';
|
||||
import { MigrationModePage } from './migrationModePage';
|
||||
import { SqlSourceConfigurationPage } from './sqlSourceConfigurationPage';
|
||||
|
||||
export const WIZARD_INPUT_COMPONENT_WIDTH = '400px';
|
||||
export class WizardController {
|
||||
@@ -39,6 +40,7 @@ export class WizardController {
|
||||
// const subscriptionSelectionPage = new SubscriptionSelectionPage(wizard, stateModel);
|
||||
const migrationModePage = new MigrationModePage(wizard, stateModel);
|
||||
const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel);
|
||||
const sourceConfigurationPage = new SqlSourceConfigurationPage(wizard, stateModel);
|
||||
const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel);
|
||||
const integrationRuntimePage = new IntergrationRuntimePage(wizard, stateModel);
|
||||
const summaryPage = new SummaryPage(wizard, stateModel);
|
||||
@@ -46,6 +48,7 @@ export class WizardController {
|
||||
const pages: MigrationWizardPage[] = [
|
||||
// subscriptionSelectionPage,
|
||||
azureAccountsPage,
|
||||
sourceConfigurationPage,
|
||||
skuRecommendationPage,
|
||||
migrationModePage,
|
||||
databaseBackupPage,
|
||||
@@ -95,7 +98,7 @@ export function createInformationRow(view: azdata.ModelView, label: string, valu
|
||||
})
|
||||
.withItems(
|
||||
[
|
||||
creaetLabelTextComponent(view, label),
|
||||
createLabelTextComponent(view, label),
|
||||
createTextCompononent(view, value)
|
||||
],
|
||||
{
|
||||
@@ -114,11 +117,9 @@ export function createHeadingTextComponent(view: azdata.ModelView, value: string
|
||||
}
|
||||
|
||||
|
||||
export function creaetLabelTextComponent(view: azdata.ModelView, value: string): azdata.TextComponent {
|
||||
export function createLabelTextComponent(view: azdata.ModelView, value: string, styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
|
||||
const component = createTextCompononent(view, value);
|
||||
component.updateCssStyles({
|
||||
'width': '300px'
|
||||
});
|
||||
component.updateCssStyles(styles);
|
||||
return component;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user