diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts new file mode 100644 index 0000000000..6d1cfadce9 --- /dev/null +++ b/extensions/sql-migration/src/api/utils.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function deepClone(obj: T): T { + if (!obj || typeof obj !== 'object') { + return obj; + } + if (obj instanceof RegExp) { + // See https://github.com/Microsoft/TypeScript/issues/10990 + return obj as any; + } + const result: any = Array.isArray(obj) ? [] : {}; + Object.keys(obj).forEach((key: string) => { + if ((obj)[key] && typeof (obj)[key] === 'object') { + result[key] = deepClone((obj)[key]); + } else { + result[key] = (obj)[key]; + } + }); + return result; +} diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index c257ce6454..7991e8b7c0 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -46,6 +46,7 @@ export const ACCOUNT_LINK_BUTTON_LABEL = localize('sql.migration.wizard.account. export function accountLinkedMessage(count: number): string { return count === 1 ? localize('sql.migration.wizard.account.count.single.message', '{0} account linked', count) : localize('sql.migration.wizard.account.count.multiple.message', '{0} accounts linked', count); } +export const AZURE_TENANT = localize('sql.migration.azure.tenant', "Azure AD tenant"); // database backup page diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index e18c0817d6..8734e6687b 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -5,6 +5,7 @@ import * as azdata from 'azdata'; import { azureResource } from 'azureResource'; +import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, SqlMigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer } from '../api/azure'; @@ -79,6 +80,7 @@ export interface StateChangeEvent { export class MigrationStateModel implements Model, vscode.Disposable { public _azureAccounts!: azdata.Account[]; public _azureAccount!: azdata.Account; + public _accountTenants!: azurecore.Tenant[]; public _subscriptions!: azureResource.AzureResourceSubscription[]; @@ -197,6 +199,19 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._azureAccounts[index]; } + public getTenantValues(): azdata.CategoryValue[] { + return this._accountTenants.map(tenant => { + return { + displayName: tenant.displayName, + name: tenant.id + }; + }); + } + + public getTenant(index: number): azurecore.Tenant { + return this._accountTenants[index]; + } + public async getSubscriptionsDropdownValues(): Promise { let subscriptionsValues: azdata.CategoryValue[] = []; try { diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index 015907f4d4..ba3c15f2de 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -9,9 +9,12 @@ import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; +import { deepClone } from '../api/utils'; export class AccountsSelectionPage extends MigrationWizardPage { private _azureAccountsDropdown!: azdata.DropDownComponent; + private _accountTenantDropdown!: azdata.DropDownComponent; + private _accountTenantFlexContainer!: azdata.FlexContainer; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.ACCOUNTS_SELECTION_PAGE_TITLE), migrationStateModel); @@ -22,7 +25,8 @@ export class AccountsSelectionPage extends MigrationWizardPage { const form = view.modelBuilder.formContainer() .withFormItems( [ - await this.createAzureAccountsDropdown(view) + await this.createAzureAccountsDropdown(view), + await this.createAzureTenantContainer(view), ] ); await view.initializeModel(form.component()); @@ -48,10 +52,24 @@ export class AccountsSelectionPage extends MigrationWizardPage { this._azureAccountsDropdown.onValueChanged(async (value) => { if (value.selected) { - this.migrationStateModel._azureAccount = this.migrationStateModel.getAccount(value.index); + const selectedAzureAccount = this.migrationStateModel.getAccount(value.index); + // Making a clone of the account object to preserve the original tenants + this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount); + if (this.migrationStateModel._azureAccount.properties.tenants.length > 1) { + this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants; + this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues(); + this._accountTenantFlexContainer.updateCssStyles({ + 'display': 'inline' + }); + } else { + this._accountTenantFlexContainer.updateCssStyles({ + 'display': 'none' + }); + } this.migrationStateModel._subscriptions = undefined!; this.migrationStateModel._targetSubscription = undefined!; this.migrationStateModel._databaseBackup.subscription = undefined!; + } }); @@ -71,7 +89,7 @@ export class AccountsSelectionPage extends MigrationWizardPage { .withLayout({ flexFlow: 'column' }) - .withItems([this._azureAccountsDropdown, linkAccountButton], { CSSStyles: { 'margin': '2px', } }) + .withItems([this._azureAccountsDropdown, linkAccountButton]) .component(); return { @@ -80,6 +98,50 @@ export class AccountsSelectionPage extends MigrationWizardPage { }; } + private createAzureTenantContainer(view: azdata.ModelView): azdata.FormComponent { + + const azureTenantDropdownLabel = view.modelBuilder.text().withProps({ + value: constants.AZURE_TENANT, + CSSStyles: { + 'margin': '0px' + } + }).component(); + + this._accountTenantDropdown = view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH + }).component(); + + this._accountTenantDropdown.onValueChanged(value => { + /** + * Replacing all the tenants in azure account with the tenant user has selected. + * All azure requests will only run on this tenant from now on + */ + if (value.selected) { + this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel.getTenant(value.index)]; + } + }); + + this._accountTenantFlexContainer = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column' + }) + .withItems([ + azureTenantDropdownLabel, + this._accountTenantDropdown + ]) + .withProps({ + CSSStyles: { + 'display': 'none' + } + }) + .component(); + + return { + title: '', + component: this._accountTenantFlexContainer + }; + } + private async populateAzureAccountsDropdown(): Promise { this._azureAccountsDropdown.loading = true; try {