diff --git a/extensions/sql-migration/config.json b/extensions/sql-migration/config.json index 4404cb7c8b..57ab29e29f 100644 --- a/extensions/sql-migration/config.json +++ b/extensions/sql-migration/config.json @@ -1,7 +1,7 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.migration-{#fileName#}", "useDefaultLinuxRuntime": true, - "version": "4.5.0.38", + "version": "4.7.0.3", "downloadFileNames": { "Windows_86": "win-x86-net7.0.zip", "Windows": "win-x64-net7.0.zip", diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 2edda5db8d..37185ab602 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": "1.4.2", + "version": "1.4.3", "publisher": "Microsoft", "preview": false, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index ecb549313f..d435f658a6 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -111,6 +111,7 @@ export const SKU_RECOMMENDATION_ASSESSMENT_ERROR_BYPASS = localize('sql.migratio export const SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL = localize('sql.migration.wizard.sku.assessment.error.detail', '[There are no assessment results to validate readiness of your database migration. By checking this box, you acknowledge you want to proceed migrating your database to the desired Azure SQL target.]',); export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.assessment.button.label', "Refresh assessment"); export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target"); +export const SKU_RECOMMENDATION_CHOOSE_A_TARGET_HELP = localize('sql.migration.wizard.sku.choose_a_target.help', "Not sure which Azure SQL target is right for you? Learn more"); export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance"); export const SKU_RECOMMENDATION_SQLDB_CARD_TEXT = localize('sql.migration.sku.sqldb.card.title', "Azure SQL Database"); @@ -582,8 +583,12 @@ export const DATABASE_BACKUP_BLOB_STORAGE_TABLE_HELP_TEXT = localize('sql.migrat export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Subscription"); export const DATABASE_BACKUP_MIGRATION_MODE_LABEL = localize('sql.migration.database.migration.mode.label', "Migration mode"); export const DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION = localize('sql.migration.database.migration.mode.description', "To migrate to the Azure SQL target, choose a migration mode based on your downtime requirements."); -export const DATABASE_TABLE_SELECTION_LABEL = localize('sql.migration.database.table.selection.label', "Migration table selection"); -export const DATABASE_TABLE_SELECTION_DESCRIPTION = localize('sql.migration.database.table.selection.description', "To migrate to the Azure SQL target, select tables in each database for migration."); +export const DATABASE_TABLE_SELECTION_LABEL = localize('sql.migration.database.table.selection.label', "Table selection"); +export const DATABASE_TABLE_SELECTION_DESCRIPTION = localize('sql.migration.database.table.selection.description', "For each database below, click Edit to select the tables to migrate from source to target. Then, before clicking Next, validate the provided configuration by clicking 'Run validation'."); +export const DATABASE_SCHEMA_MIGRATION_HELP = localize('sql.migration.database.schema.migration.help', "Make sure to migrate the database schema from source to target by using the {0} or the {1} in Azure Data Studio before selecting the list of tables to migrate."); +export const DATABASE_SCHEMA_MIGRATION_DACPAC_EXTENSION = localize('sql.migration.database.schema.migration.dacpac', "SQL Server dacpac extension"); +export const DATABASE_SCHEMA_MIGRATION_PROJECTS_EXTENSION = localize('sql.migration.database.schema.migration.project', "SQL Database Projects extension"); + export const DATABASE_TABLE_REFRESH_LABEL = localize('sql.migration.database.table.refresh.label', "Refresh"); export const DATABASE_TABLE_SOURCE_DATABASE_COLUMN_LABEL = localize('sql.migration.database.table.source.column.label', "Source database"); export const DATABASE_TABLE_TARGET_DATABASE_COLUMN_LABEL = localize('sql.migration.database.table.target.column.label', "Target database"); @@ -655,12 +660,16 @@ export const INVALID_NETWORK_SHARE_LOCATION = localize('sql.migration.invalid.ne export const INVALID_USER_ACCOUNT = localize('sql.migration.invalid.user.account', "Invalid user account format. Example: {0}", WINDOWS_USER_ACCOUNT); export const INVALID_TARGET_NAME_ERROR = localize('sql.migration.invalid.target.name.error', "Enter a valid name for the target database."); export const PROVIDE_UNIQUE_CONTAINERS = localize('sql.migration.provide.unique.containers', "Provide a unique container for each target database. Databases affected: "); -export function SQL_SOURCE_DETAILS(authMethod: MigrationSourceAuthenticationType, serverName: string): string { +export function SQL_SOURCE_DETAILS(authMethod: MigrationSourceAuthenticationType, serverName: string, isSqlDbScenario: boolean = false): string { switch (authMethod) { case MigrationSourceAuthenticationType.Integrated: - return localize('sql.migration.source.details.windowAuth', "Enter the Windows Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance and identify valid backup files.", serverName); + return isSqlDbScenario + ? localize('sql.migration.source.details.windowAuth.db', "Enter the Windows Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance from the self-hosted integration runtime.", serverName) + : localize('sql.migration.source.details.windowAuth.nonDb', "Enter the Windows Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance and identify valid backup files.", serverName); case MigrationSourceAuthenticationType.Sql: - return localize('sql.migration.source.details.sqlAuth', "Enter the SQL Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance and identify valid backup files.", serverName); + return isSqlDbScenario + ? localize('sql.migration.source.details.sqlAuth.db', "Enter the SQL Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance from the self-hosted integration runtime.", serverName) + : localize('sql.migration.source.details.sqlAuth.nonDb', "Enter the SQL Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance and identify valid backup files.", serverName); } } export const SELECT_RESOURCE_GROUP_PROMPT = localize('sql.migration.blob.resourceGroup.select.prompt', "Select a resource group value first."); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index c152e1441b..e725fece1c 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -69,7 +69,8 @@ export class SqlDatabaseTree { private _objectDetailsType!: azdata.TextComponent; private _objectDetailsName!: azdata.TextComponent; private _objectDetailsSample!: azdata.TextComponent; - private _moreInfo!: azdata.HyperlinkComponent; + private _moreInfoTitle!: azdata.TextComponent; + private _moreInfoText!: azdata.HyperlinkComponent; private _assessmentTitle!: azdata.TextComponent; private _databaseTableValues!: azdata.DeclarativeTableCellValue[][]; @@ -626,12 +627,13 @@ export class SqlDatabaseTree { .withProps({ CSSStyles: textStyle }).component(); - const moreInfo = this._view.modelBuilder.text() + + this._moreInfoTitle = this._view.modelBuilder.text() .withProps({ value: constants.MORE_INFO, CSSStyles: LABEL_CSS }).component(); - this._moreInfo = this._view.modelBuilder.hyperlink() + this._moreInfoText = this._view.modelBuilder.hyperlink() .withProps({ label: '', url: '', @@ -645,8 +647,8 @@ export class SqlDatabaseTree { this._descriptionText, recommendationTitle, this._recommendationText, - moreInfo, - this._moreInfo]) + this._moreInfoTitle, + this._moreInfoText]) .withLayout({ flexFlow: 'column' }) .component(); @@ -811,14 +813,31 @@ export class SqlDatabaseTree { } public async refreshAssessmentDetails(selectedIssue?: SqlMigrationAssessmentResultItem): Promise { - this._assessmentTitle.value = selectedIssue?.checkId || ''; - this._descriptionText.value = selectedIssue?.description || ''; - this._moreInfo.url = selectedIssue?.helpLink || ''; - this._moreInfo.label = selectedIssue?.displayName || ''; - this._moreInfo.ariaLabel = selectedIssue?.displayName || ''; - this._impactedObjects = selectedIssue?.impactedObjects || []; - this._recommendationText.value = selectedIssue?.message || constants.NA; + await this._assessmentTitle.updateProperty('value', selectedIssue?.checkId || ''); + await this._descriptionText.updateProperty('value', selectedIssue?.description || ''); + await this._recommendationText.updateProperty('value', selectedIssue?.message || constants.NA); + if (selectedIssue?.helpLink) { + await this._moreInfoTitle.updateProperty('display', 'flex'); + await this._moreInfoText.updateProperties({ + 'display': 'flex', + 'url': selectedIssue?.helpLink || '', + 'label': selectedIssue?.displayName || '', + 'ariaLabel': selectedIssue?.displayName || '', + 'showLinkIcon': true + }); + } else { + await this._moreInfoTitle.updateProperty('display', 'none'); + await this._moreInfoText.updateProperties({ + 'display': 'none', + 'url': '', + 'label': '', + 'ariaLabel': '', + 'showLinkIcon': false + }); + } + + this._impactedObjects = selectedIssue?.impactedObjects || []; await this._impactedObjectsTable.setDataValues( this._impactedObjects.map( (object) => [{ value: object.objectType }, { value: object.name }])); diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 53c23f7b4d..051659bd43 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -892,7 +892,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { : undefined!; this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS( this.migrationStateModel._authenticationType, - connectionProfile.serverName); + connectionProfile.serverName, + isSqlDbTarget); this._sqlSourceUsernameInput.value = username; this._sqlSourcePassword.value = (await getSourceConnectionCredentials()).password; @@ -1209,7 +1210,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { let uniqueBackupLocations = [...new Set(backupLocations)]; - if (uniqueBackupLocations.length !== backupLocations.length) { + if (!isSqlDbTarget && uniqueBackupLocations.length !== backupLocations.length) { this.wizard.message = { level: azdata.window.MessageLevel.Warning, text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_WARNING, @@ -1242,7 +1243,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { let uniqueBackupLocations = [...new Set(backupLocations)]; - if (uniqueBackupLocations.length !== backupLocations.length) { + if (!isSqlDbTarget && uniqueBackupLocations.length !== backupLocations.length) { this.wizard.message = { level: azdata.window.MessageLevel.Warning, text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_WARNING, @@ -1656,6 +1657,32 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private _migrationTableSelectionContainer(): azdata.FlexContainer { + const tableMappingHeader = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_TABLE_SELECTION_LABEL, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.SECTION_HEADER_CSS } + }).component(); + + const tableMappingText = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_TABLE_SELECTION_DESCRIPTION, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS } + }).component(); + + const tableMappingInfoBox = this._view.modelBuilder.infoBox() + .withProps({ + text: constants.DATABASE_SCHEMA_MIGRATION_HELP, + style: 'information', + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS, 'margin': '5px 0 0 0' }, + links: [ + { text: constants.DATABASE_SCHEMA_MIGRATION_DACPAC_EXTENSION, url: 'https://learn.microsoft.com/sql/azure-data-studio/extensions/sql-server-dacpac-extension' }, + { text: constants.DATABASE_SCHEMA_MIGRATION_PROJECTS_EXTENSION, url: 'https://learn.microsoft.com/sql/azure-data-studio/extensions/sql-database-project-extension' }, + ] + }).component(); + this._refreshButton = this._view.modelBuilder.button() .withProps({ buttonType: azdata.ButtonType.Normal, @@ -1739,6 +1766,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { return this._view.modelBuilder.flexContainer() .withItems([ + tableMappingHeader, + tableMappingText, + tableMappingInfoBox, this._refreshLoading, this._databaseTable]) .withLayout({ flexFlow: 'column' }) diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 5985c39130..32370c6f9d 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -246,6 +246,14 @@ export class SKURecommendationPage extends MigrationWizardPage { } }).component(); + const learnMoreLink = this._view.modelBuilder.hyperlink() + .withProps({ + label: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET_HELP, + ariaLabel: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET_HELP, + url: 'https://learn.microsoft.com/azure/azure-sql/azure-sql-iaas-vs-paas-what-is-overview', + showLinkIcon: true, + }).component(); + this._rbg = this._view!.modelBuilder.radioCardGroup().withProps({ cards: [], iconHeight: '35px', @@ -354,7 +362,7 @@ export class SKURecommendationPage extends MigrationWizardPage { .component(); const component = this._view.modelBuilder.divContainer() - .withItems([chooseYourTargetText, this._rbgLoader]) + .withItems([chooseYourTargetText, learnMoreLink, this._rbgLoader]) .component(); return component; }