diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index dc496740b2..f2b8450d84 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.0", + "version": "1.4.1", "publisher": "Microsoft", "preview": false, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index 2209d72157..c03e55d55a 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -705,6 +705,15 @@ export async function getAzureSqlMigrationServices(account?: Account, subscripti return []; } +export interface Blob { + resourceGroup: azureResource.AzureResourceResourceGroup; + storageAccount: azureResource.AzureGraphResource; + blobContainer: azureResource.BlobContainer; + storageKey: string; + lastBackupFile?: string; + folderName?: string; +} + export async function getBlobContainer(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount): Promise { let blobContainers: azureResource.BlobContainer[] = []; try { @@ -722,7 +731,14 @@ export async function getBlobLastBackupFileNames(account?: Account, subscription let lastFileNames: azureResource.Blob[] = []; try { if (account && subscription && storageAccount && blobContainer) { - lastFileNames = await azure.getBlobs(account, subscription, storageAccount, blobContainer.name); + const blobs = await azure.getBlobs(account, subscription, storageAccount, blobContainer.name); + + blobs.forEach(blob => { + // only show at most one folder deep + if ((blob.name.split('/').length === 1 || blob.name.split('/').length === 2) && !lastFileNames.includes(blob)) { + lastFileNames.push(blob); + } + }); } } catch (e) { logError(TelemetryViews.Utils, 'utils.getBlobLastBackupFileNames', e); @@ -731,6 +747,64 @@ export async function getBlobLastBackupFileNames(account?: Account, subscription return lastFileNames; } +export async function getBlobFolders(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount, blobContainer?: azureResource.BlobContainer): Promise { + let folders: string[] = []; + try { + if (account && subscription && storageAccount && blobContainer) { + const blobs = await azure.getBlobs(account, subscription, storageAccount, blobContainer.name); + + blobs.forEach(blob => { + let folder: string = ''; + + if (blob.name.split('/').length === 1) { + folder = '/'; // no folder (root) + } else if (blob.name.split('/').length === 2) { + folder = blob.name.split('/')[0]; // one folder deep + } + + if (folder && !folders.includes(folder)) { + folders.push(folder); + } + }); + } + } catch (e) { + logError(TelemetryViews.Utils, 'utils.getBlobLastBackupFolders', e); + } + folders.sort(); + return folders; +} + +export function getBlobContainerNameWithFolder(blob: Blob, isOfflineMigration: boolean): string { + const blobContainerName = blob.blobContainer.name; + + if (isOfflineMigration) { + const lastBackupFile = blob.lastBackupFile; + if (!lastBackupFile || lastBackupFile.split('/').length !== 2) { + return blobContainerName; + } + + // for offline scenario, take the folder name out of the blob name and add it to the container name instead + return blobContainerName + '/' + lastBackupFile.split('/')[0]; + } else { + const folderName = blob.folderName; + if (!folderName || folderName === '/' || folderName === 'undefined') { + return blobContainerName; + } + + // for online scenario, take the explicitly provided folder name + return blobContainerName + '/' + folderName; + } +} + +export function getLastBackupFileNameWithoutFolder(blob: Blob) { + const lastBackupFile = blob.lastBackupFile; + if (!lastBackupFile || lastBackupFile.split('/').length !== 2) { + return lastBackupFile; + } + + return lastBackupFile.split('/')[1]; +} + export function getAzureResourceDropdownValues( azureResources: { location: string, id: string, name: string }[], location: azureResource.AzureLocation | undefined, @@ -762,12 +836,16 @@ export function getResourceDropdownValues(resources: { id: string, name: string || [{ name: '', displayName: resourceNotFoundMessage }]; } -export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise { +export function getAzureTenantsDropdownValues(tenants: Tenant[]): CategoryValue[] { + if (!tenants || !tenants.length) { + return [{ name: '', displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR }]; + } + return tenants?.map(tenant => { return { name: tenant.id, displayName: tenant.displayName }; }) || [{ name: '', displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR }]; } -export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise { +export function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): CategoryValue[] { if (!locations || !locations.length) { return [{ name: '', displayName: constants.NO_LOCATION_FOUND }]; } @@ -776,11 +854,24 @@ export async function getAzureLocationsDropdownValues(locations: azureResource.A || [{ name: '', displayName: constants.NO_LOCATION_FOUND }]; } -export async function getBlobLastBackupFileNamesValues(blobs: azureResource.Blob[]): Promise { +export function getBlobLastBackupFileNamesValues(blobs: azureResource.Blob[]): CategoryValue[] { + if (!blobs || !blobs.length) { + return [{ name: '', displayName: constants.NO_BLOBFILES_FOUND }]; + } + return blobs?.map(blob => { return { name: blob.name, displayName: blob.name }; }) || [{ name: '', displayName: constants.NO_BLOBFILES_FOUND }]; } +export function getBlobFolderValues(folders: string[]): CategoryValue[] { + if (!folders || !folders.length) { + return [{ name: '', displayName: constants.NO_BLOBFOLDERS_FOUND }]; + } + + return folders?.map(folder => { return { name: folder, displayName: folder }; }) + || [{ name: '', displayName: constants.NO_BLOBFOLDERS_FOUND }]; +} + export async function updateControlDisplay(control: Component, visible: boolean, displayStyle: DisplayType = 'inline'): Promise { const display = visible ? displayStyle : 'none'; control.display = display; diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 6a9bfc4520..dbecd85768 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -603,6 +603,7 @@ export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccoun export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found."); export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found."); export const NO_BLOBFILES_FOUND = localize('sql.migration.no.blobFiles.found', "No blob files found."); +export const NO_BLOBFOLDERS_FOUND = localize('sql.migration.no.blobFolders.found', "No blob folders found."); export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "To continue, select a valid subscription."); export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "To continue, select a valid location."); export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "To continue, select a valid resource group."); @@ -635,6 +636,9 @@ export function INVALID_BLOB_CONTAINER_ERROR(sourceDb: string): string { export function INVALID_BLOB_LAST_BACKUP_FILE_ERROR(sourceDb: string): string { return localize('sql.migration.invalid.blob.lastBackupFile.error', "To continue, select a valid last backup file for source database '{0}'.", sourceDb); } +export function INVALID_BLOB_LAST_BACKUP_FOLDER_ERROR(sourceDb: string): string { + return localize('sql.migration.invalid.blob.lastBackupFolder.error', "To continue, select a valid backup folder for source database '{0}'.", sourceDb); +} export function INVALID_NON_PAGE_BLOB_BACKUP_FILE_ERROR(sourceDb: string): string { return localize('sql.migration.invalid.non.page.blob.backupFile.error', "To continue, select a blob container where all the backup files are page blobs for source database '{0}', as block blobs are supported only for targets running SQL Server 2016 or later. Learn more: https://aka.ms/dms-migrations-troubleshooting", sourceDb); } @@ -907,6 +911,7 @@ export const NETWORK_SHARE = localize('sql.migration.network.share', "Network sh export const NETWORK_SHARE_PATH = localize('sql.migration.network.share.path', "Network share path"); export const BLOB_CONTAINER = localize('sql.migration.blob.container.title', "Blob container"); export const BLOB_CONTAINER_LAST_BACKUP_FILE = localize('sql.migration.blob.container.last.backup.file.label', "Last backup file"); +export const BLOB_CONTAINER_FOLDER = localize('sql.migration.blob.container.folder.label', "Folder"); export const BLOB_CONTAINER_RESOURCE_GROUP = localize('sql.migration.blob.container.label', "Blob container resource group"); export const BLOB_CONTAINER_STORAGE_ACCOUNT = localize('sql.migration.blob.container.storage.account.label', "Blob container storage account"); export const SOURCE_DATABASES = localize('sql.migration.source.databases', "Source databases"); diff --git a/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts index c572723e64..4545817c9d 100644 --- a/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts @@ -372,7 +372,7 @@ export class SelectMigrationServiceDialog { try { this._accountTenantDropdown.loading = true; this._accountTenants = utils.getAzureTenants(this._serviceContext.azureAccount); - this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this._accountTenants); + this._accountTenantDropdown.values = utils.getAzureTenantsDropdownValues(this._accountTenants); await this._accountTenantFlexContainer.updateCssStyles( this._accountTenants.length > 1 ? STYLE_ShOW @@ -426,7 +426,7 @@ export class SelectMigrationServiceDialog { this._serviceContext.subscription, this._sqlMigrationServices); - this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this._locations); + this._azureLocationDropdown.values = utils.getAzureLocationsDropdownValues(this._locations); if (this._azureLocationDropdown.values.length > 0) { utils.selectDefaultDropdownValue( this._azureLocationDropdown, diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 15026ee63e..29d5f2e59c 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -13,7 +13,7 @@ import * as constants from '../constants/strings'; import * as nls from 'vscode-nls'; import { v4 as uuidv4 } from 'uuid'; import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemetry'; -import { hashString, deepClone } from '../api/utils'; +import { hashString, deepClone, getBlobContainerNameWithFolder, Blob, getLastBackupFileNameWithoutFolder } from '../api/utils'; import { SKURecommendationPage } from '../wizard/skuRecommendationPage'; import { excludeDatabases, getEncryptConnectionValue, getSourceConnectionId, getSourceConnectionProfile, getSourceConnectionServerInfo, getSourceConnectionString, getSourceConnectionUri, getTrustServerCertificateValue, SourceDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils'; import { LoginMigrationModel } from './loginMigrationModel'; @@ -126,14 +126,6 @@ export interface NetworkShare { storageKey: string; } -export interface Blob { - resourceGroup: azurecore.azureResource.AzureResourceResourceGroup; - storageAccount: StorageAccount; - blobContainer: azurecore.azureResource.BlobContainer; - storageKey: string; - lastBackupFile?: string; // _todo: does it make sense to store the last backup file here? -} - export interface Model { readonly currentState: State; gatheringInformationError: string | undefined; @@ -206,6 +198,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _fileShares!: azurecore.azureResource.FileShare[]; public _blobContainers!: azurecore.azureResource.BlobContainer[]; public _lastFileNames!: azurecore.azureResource.Blob[]; + public _blobContainerFolders!: string[]; public _sourceDatabaseNames!: string[]; public _targetDatabaseNames!: string[]; @@ -1086,7 +1079,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { azureBlob: { storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id, accountKey: this._databaseBackup.blobs[i].storageKey, - blobContainerName: this._databaseBackup.blobs[i].blobContainer.name + blobContainerName: getBlobContainerNameWithFolder(this._databaseBackup.blobs[i], isOfflineMigration) } } }; @@ -1094,7 +1087,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { if (isOfflineMigration) { requestBody.properties.offlineConfiguration = { offline: isOfflineMigration, - lastBackupName: this._databaseBackup.blobs[i]?.lastBackupFile + lastBackupName: getLastBackupFileNameWithoutFolder(this._databaseBackup.blobs[i]) }; } break; diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index bd26b68f56..3292ba50ef 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { EOL } from 'os'; import { getStorageAccountAccessKeys, SqlVMServer } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, NetworkShare, StateChangeEvent, ValidateIrState, ValidationResult } from '../models/stateMachine'; +import { MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, NetworkShare, StateChangeEvent, ValidateIrState, ValidationResult } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { IconPathHelper } from '../constants/iconPathHelper'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; @@ -27,6 +27,7 @@ const blobResourceGroupErrorStrings = [constants.RESOURCE_GROUP_NOT_FOUND]; const blobStorageAccountErrorStrings = [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT]; const blobContainerErrorStrings = [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT]; const blobFileErrorStrings = [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER]; +const blobFolderErrorStrings = [constants.NO_BLOBFOLDERS_FOUND, constants.SELECT_BLOB_CONTAINER]; export class DatabaseBackupPage extends MigrationWizardPage { private _view!: azdata.ModelView; @@ -46,6 +47,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _blobContainerStorageAccountDropdowns!: azdata.DropDownComponent[]; private _blobContainerDropdowns!: azdata.DropDownComponent[]; private _blobContainerLastBackupFileDropdowns!: azdata.DropDownComponent[]; + private _blobContainerFolderDropdowns!: azdata.DropDownComponent[]; private _blobContainerVmDatabaseAlreadyExistsInfoBox!: azdata.TextComponent; private _networkShareStorageAccountDetails!: azdata.FlexContainer; @@ -390,7 +392,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.NETWORK_SHARE_PATH, @@ -412,7 +414,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH, + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL, }, { displayName: constants.TARGET_DATABASE_NAME, @@ -420,7 +422,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.RESOURCE_GROUP, @@ -428,7 +430,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.STORAGE_ACCOUNT, @@ -436,7 +438,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.BLOB_CONTAINER, @@ -444,7 +446,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL }, { displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, @@ -452,9 +454,18 @@ export class DatabaseBackupPage extends MigrationWizardPage { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles, isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH, + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL, hidden: true - } + }, + { + displayName: constants.BLOB_CONTAINER_FOLDER, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH_SMALL, + hidden: true + }, ] }).component(); @@ -719,6 +730,12 @@ export class DatabaseBackupPage extends MigrationWizardPage { errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index])); } }); + } else { + this._blobContainerFolderDropdowns.forEach((v, index) => { + if (this.shouldDisplayBlobDropdownError(v, blobFolderErrorStrings)) { + errors.push(constants.INVALID_BLOB_LAST_BACKUP_FOLDER_ERROR(this.migrationStateModel._databasesForMigration[index])); + } + }); } if (this.migrationStateModel.isSqlVmTarget && utils.isTargetSqlVm2014OrBelow(this.migrationStateModel._targetServerInstance as SqlVMServer)) { @@ -784,21 +801,32 @@ export class DatabaseBackupPage extends MigrationWizardPage { } try { const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE; - const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1; - const oldHidden = this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden; - const newHidden = !isOfflineMigration; - if (oldHidden !== newHidden) { + + // for offline migrations, show last backup file column + const lastBackupFileColumnIndex = 5; + const lastBackupFileColumnOldHidden = this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden; + const lastBackupFileColumnNewHidden = !isOfflineMigration; + if (lastBackupFileColumnOldHidden !== lastBackupFileColumnNewHidden) { // clear values prior to hiding columns if changing column visibility // to prevent null DeclarativeTableComponent - exception / _view null await this._blobContainerTargetDatabaseNamesTable.setDataValues([]); } + this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = lastBackupFileColumnNewHidden; + + // for online migrations, show folder column + const folderColumnIndex = 6; + const folderColumnOldHidden = this._blobContainerTargetDatabaseNamesTable.columns[folderColumnIndex].hidden; + const folderColumnNewHidden = isOfflineMigration; + if (folderColumnOldHidden !== folderColumnNewHidden) { + // clear values prior to hiding columns if changing column visibility + // to prevent null DeclarativeTableComponent - exception / _view null + await this._blobContainerTargetDatabaseNamesTable.setDataValues([]); + } + this._blobContainerTargetDatabaseNamesTable.columns[folderColumnIndex].hidden = folderColumnNewHidden; + + + - this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = newHidden; - this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => { - column.width = isOfflineMigration - ? WIZARD_TABLE_COLUMN_WIDTH_SMALL - : WIZARD_TABLE_COLUMN_WIDTH; - }); const connectionProfile = await getSourceConnectionProfile(); const queryProvider = await getSourceConnectionQueryProvider(); @@ -841,6 +869,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerStorageAccountDropdowns = []; this._blobContainerDropdowns = []; this._blobContainerLastBackupFileDropdowns = []; + this._blobContainerFolderDropdowns = []; if (this.migrationStateModel.isSqlMiTarget) { this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); @@ -862,7 +891,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { ? this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? sourceDatabaseName : sourceDatabaseName; let networkShare = {}; - let blob = {}; + let blob = {}; if (this.migrationStateModel._didUpdateDatabasesForMigration || this.migrationStateModel._didDatabaseMappingChange) { @@ -1013,6 +1042,15 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true, enabled: false, }).component(); + const blobContainerFolderDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.BLOB_CONTAINER_FOLDER, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + this._disposables.push( blobContainerResourceDropdown.onValueChanged(async (value) => { @@ -1070,6 +1108,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { await this.loadBlobLastBackupFileDropdown(index); await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); + } else { + await this.loadBlobFolderDropdown(index); + await blobContainerFolderDropdown.updateProperties({ enabled: true }); } } else { await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); @@ -1091,6 +1132,17 @@ export class DatabaseBackupPage extends MigrationWizardPage { } })); this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown); + } else { + this._disposables.push( + blobContainerFolderDropdown.onValueChanged(value => { + if (value && value !== 'undefined') { + if (this.migrationStateModel._blobContainerFolders.includes(value) && !blobFolderErrorStrings.includes(value)) { + const selectedFolder = value; + this.migrationStateModel._databaseBackup.blobs[index].folderName = selectedFolder; + } + } + })); + this._blobContainerFolderDropdowns.push(blobContainerFolderDropdown); } }); this.migrationStateModel._sourceDatabaseNames = this.migrationStateModel._databasesForMigration; @@ -1109,7 +1161,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { { value: this._blobContainerResourceGroupDropdowns[index] }, { value: this._blobContainerStorageAccountDropdowns[index] }, { value: this._blobContainerDropdowns[index] }, - { value: this._blobContainerLastBackupFileDropdowns[index] }]); + { value: this._blobContainerLastBackupFileDropdowns[index] }, + { value: this._blobContainerFolderDropdowns[index] }]); await this._blobContainerTargetDatabaseNamesTable.setDataValues([]); await this._blobContainerTargetDatabaseNamesTable.setDataValues(blobContainerTargetData); await this.getSubscriptionValues(); @@ -1266,6 +1319,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { await this._blobContainerLastBackupFileDropdowns[i]?.validate(); + } else { + await this._blobContainerFolderDropdowns[i]?.validate(); } } if (this.migrationStateModel.isIrMigration) { @@ -1420,7 +1475,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); - dropDown.values = await utils.getBlobLastBackupFileNamesValues( + dropDown.values = utils.getBlobLastBackupFileNamesValues( this.migrationStateModel._lastFileNames); utils.selectDefaultDropdownValue( dropDown, @@ -1434,6 +1489,28 @@ export class DatabaseBackupPage extends MigrationWizardPage { } } + private async loadBlobFolderDropdown(index: number): Promise { + const dropDown = this._blobContainerFolderDropdowns[index]; + if (dropDown) { + try { + dropDown.loading = true; + this.migrationStateModel._blobContainerFolders = await utils.getBlobFolders(this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription, + this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, + this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); + dropDown.values = utils.getBlobFolderValues(this.migrationStateModel._blobContainerFolders); + utils.selectDefaultDropdownValue( + dropDown, + this.migrationStateModel._blobContainerFolders[0], + false); + } catch (error) { + logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobFolders', error); + } finally { + dropDown.loading = false; + } + } + } + private shouldDisplayBlobDropdownError(v: azdata.DropDownComponent, errorStrings: string[]) { return v.value === undefined || errorStrings.includes((v.value)?.displayName); } @@ -1446,6 +1523,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerLastBackupFileDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER); utils.selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[rowIndex], 0); await this._blobContainerLastBackupFileDropdowns[rowIndex]?.updateProperties(dropdownProps); + } else { + this._blobContainerFolderDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER); + utils.selectDropDownIndex(this._blobContainerFolderDropdowns[rowIndex], 0); + await this._blobContainerFolderDropdowns[rowIndex]?.updateProperties(dropdownProps); } if (columnName === constants.BLOB_CONTAINER) { return; } diff --git a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts index 8fa7c2a791..e994e83b55 100644 --- a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts @@ -865,7 +865,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { this._accountTenantDropdown.loading = true; if (this.migrationStateModel._azureAccount && this.migrationStateModel._azureAccount.isStale === false && this.migrationStateModel._azureAccount.properties.tenants.length > 0) { this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount); - this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants); + this._accountTenantDropdown.values = utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants); } utils.selectDefaultDropdownValue( this._accountTenantDropdown, @@ -929,7 +929,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { this.migrationStateModel._targetSqlDatabaseServers); break; } - this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations); + this._azureLocationDropdown.values = utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations); } catch (e) { console.log(e); } finally { diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index 3f89fdf00a..5e9e080f48 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -890,7 +890,7 @@ export class TargetSelectionPage extends MigrationWizardPage { this.migrationStateModel._azureAccount?.properties?.tenants?.length > 0) { this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount); - this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants); + this._accountTenantDropdown.values = utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants); } const tenantId = this.migrationStateModel._azureTenant?.id ?? @@ -963,7 +963,7 @@ export class TargetSelectionPage extends MigrationWizardPage { this.migrationStateModel._targetSqlDatabaseServers); break; } - this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations); + this._azureLocationDropdown.values = utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations); } catch (e) { console.log(e); } finally {