diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 5f240b9b57..e2bcffe944 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -536,6 +536,7 @@ export interface SqlMigrationAssessmentResultItem { message: string; appliesToMigrationTargetPlatform: string; issueCategory: string; + databaseName: string; impactedObjects: SqlMigrationImpactedObjectInfo[]; } diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 62e8dd34f2..37f26833f4 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -2,7 +2,7 @@ "name": "sql-migration", "displayName": "%displayName%", "description": "%description%", - "version": "0.0.3", + "version": "0.0.4", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index b190fb6e5f..fcaa9cdf3c 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -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 { 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 diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 434adec539..1991743c47 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -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"); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index c2b5acfa1d..265c260a28 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -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 { @@ -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[] = []; dialogSetupPromises.push(this.initializeDialog(this.dialog)); + azdata.window.openDialog(this.dialog); await Promise.all(dialogSetupPromises); + + await this._tree.initialize(); } } - - private parseData(model: MigrationStateModel): Map { - // 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(); - - 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; } diff --git a/extensions/sql-migration/src/dialog/assessmentResults/model/assessmentDialogComponent.ts b/extensions/sql-migration/src/dialog/assessmentResults/model/assessmentDialogComponent.ts deleted file mode 100644 index ba6caa142a..0000000000 --- a/extensions/sql-migration/src/dialog/assessmentResults/model/assessmentDialogComponent.ts +++ /dev/null @@ -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; -} diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts index 9250ebf733..76c7dd1537 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts @@ -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 { const title = this.createTitleComponent(view); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts index 368c3dd3ca..67ee8da896 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts @@ -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 { return view.modelBuilder.divContainer().withItems([ diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index f9b6972bc6..a25e62d4d2 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -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 = ['master', 'tempdb', 'msdb', 'model']; - private _model!: MigrationStateModel; - private instanceTable!: azdata.ComponentBuilder; - private databaseTable!: azdata.ComponentBuilder; - private _assessmentResultsTable!: azdata.ComponentBuilder; - private _impactedObjectsTable!: azdata.ComponentBuilder; - private _assessmentData: Map; +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, 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 { + async createComponent(view: azdata.ModelView, dbs: string[]): Promise { 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 { - - let mapRowIssue = new Map(); - 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 = 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 = 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({ - 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({ 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({ 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({ 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 { + 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; + } } diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 1b95fa0e4d..ce1a4b6269 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -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!); diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts index 5a7f7b74c7..88ed758d1f 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts @@ -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 { diff --git a/extensions/sql-migration/src/models/externalContract.ts b/extensions/sql-migration/src/models/externalContract.ts index 45f803b0be..80292543bb 100644 --- a/extensions/sql-migration/src/models/externalContract.ts +++ b/extensions/sql-migration/src/models/externalContract.ts @@ -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[]; } diff --git a/extensions/sql-migration/src/models/product.ts b/extensions/sql-migration/src/models/product.ts deleted file mode 100644 index 94bfdb9925..0000000000 --- a/extensions/sql-migration/src/models/product.ts +++ /dev/null @@ -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' - } -}; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index c252f55a46..ab75ae6d29 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -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(); 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 { + 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: [] + }; + }); + + 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 { + const sqlConnections = await azdata.connection.getConnections(); + return sqlConnections.find((value) => { + if (value.connectionId === this.sourceConnectionId) { + return true; + } else { + return false; + } + })!; + } + public async getSubscriptionsDropdownValues(): Promise { 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[]; + }[]; +} diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index f06850d281..190a448bcb 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -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); diff --git a/extensions/sql-migration/src/wizard/migrationModePage.ts b/extensions/sql-migration/src/wizard/migrationModePage.ts index 0ed8a2f913..89fd545a09 100644 --- a/extensions/sql-migration/src/wizard/migrationModePage.ts +++ b/extensions/sql-migration/src/wizard/migrationModePage.ts @@ -26,8 +26,14 @@ export class MigrationModePage extends MigrationWizardPage { } public async onPageEnter(): Promise { + this.wizard.registerNavigationValidator((e) => { + return true; + }); } public async onPageLeave(): Promise { + this.wizard.registerNavigationValidator((e) => { + return true; + }); } protected async handleStateChange(e: StateChangeEvent): Promise { } diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 97e15bb828..c2c27dba87 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -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 | undefined; - private _detailsComponent: azdata.FormComponent | undefined; - private _chooseTargetComponent: azdata.FormComponent | undefined; - private _azureSubscriptionText: azdata.FormComponent | 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 { - const component = view.modelBuilder.text().withProperties({ - 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 { - const component = view.modelBuilder.text().withProperties({ - 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({ + 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 { - const component = view.modelBuilder.text().withProperties({ - 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 { + 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 { @@ -316,7 +281,7 @@ export class SKURecommendationPage extends MigrationWizardPage { private async populateResourceInstanceDropdown(): Promise { 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 { - this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e)); - this.populateSubscriptionDropdown(); - this.constructDetails(); + public async onPageEnter(): Promise { + 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 { - 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 + }); + } + } - } + + diff --git a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts deleted file mode 100644 index 4d323053f0..0000000000 --- a/extensions/sql-migration/src/wizard/sourceConfigurationPage.ts +++ /dev/null @@ -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({ - 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 { - this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e)); - } - - public async onPageLeave(): Promise { - this.eventListener?.dispose(); - } - - protected async handleStateChange(e: StateChangeEvent): Promise { - switch (e.newState) { - case State.COLLECTION_SOURCE_INFO_ERROR: - return this.enterErrorState(); - case State.TARGET_SELECTION: - return this.enterTargetSelectionState(); - } - } - - public async canLeave(): Promise { - return this.migrationStateModel.currentState === State.TARGET_SELECTION; - } -} diff --git a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts new file mode 100644 index 0000000000..a189db21f0 --- /dev/null +++ b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts @@ -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 { + this._view = view; + const form = view.modelBuilder.formContainer() + .withFormItems( + [ + await this.createSourceCredentialContainer(), + ] + ); + await view.initializeModel(form.component()); + } + + public async onPageEnter(): Promise { + this.wizard.registerNavigationValidator((pageChangeInfo) => { + return true; + }); + } + public async onPageLeave(): Promise { + this.wizard.registerNavigationValidator((pageChangeInfo) => { + return true; + }); + } + + protected async handleStateChange(e: StateChangeEvent): Promise { + } + + private async createSourceCredentialContainer(): Promise { + + 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 + }; + } +} diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 7831a6b9ac..e816d83585 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -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; }