/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/accountDialog'; import 'vs/css!./media/accountActions'; import * as DOM from 'vs/base/browser/dom'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { Event, Emitter } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachButtonStyler, attachListStyler } from 'vs/platform/theme/common/styler'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import * as azdata from 'azdata'; import { Button } from 'sql/base/browser/ui/button/button'; import { SqlIconId } from 'sql/base/common/codicons'; import { HideReason, Modal } from 'sql/workbench/browser/modal/modal'; import { AccountViewModel } from 'sql/platform/accounts/common/accountViewModel'; import { AddAccountAction } from 'sql/platform/accounts/common/accountActions'; import { AccountListRenderer, AccountListDelegate } from 'sql/workbench/services/accountManagement/browser/accountListRenderer'; import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/platform/accounts/common/eventTypes'; import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { ILogService } from 'vs/platform/log/common/log'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { attachModalDialogStyler, attachPanelStyler } from 'sql/workbench/common/styler'; import { IViewDescriptorService, IViewsRegistry, Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Iterable } from 'vs/base/common/iterator'; import { LoadingSpinner } from 'sql/base/browser/ui/loadingSpinner/loadingSpinner'; import { Tenant, TenantListDelegate, TenantListRenderer } from 'sql/workbench/services/accountManagement/browser/tenantListRenderer'; import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces'; export const VIEWLET_ID = 'workbench.view.accountpanel'; export type AuthLibrary = 'ADAL' | 'MSAL'; export const MSAL_AUTH_LIBRARY: AuthLibrary = 'MSAL'; // default export const ADAL_AUTH_LIBRARY: AuthLibrary = 'ADAL'; export class AccountPaneContainer extends ViewPaneContainer { } export const ACCOUNT_VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, title: localize('accountExplorer.name', "Accounts"), ctorDescriptor: new SyncDescriptor(AccountPaneContainer), storageId: `${VIEWLET_ID}.state` }, ViewContainerLocation.Dialog); class AccountPanel extends ViewPane { public index?: number; private accountList?: List; private tenantList?: List; constructor( options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); } protected override renderBody(container: HTMLElement): void { this.accountList = new List('AccountList', container, new AccountListDelegate(AccountDialog.ACCOUNTLIST_HEIGHT), [this.instantiationService.createInstance(AccountListRenderer)]); this.tenantList = new List('TenantList', container, new TenantListDelegate(AccountDialog.ACCOUNTLIST_HEIGHT), [this.instantiationService.createInstance(TenantListRenderer)]); this._register(attachListStyler(this.accountList, this.themeService)); this._register(attachListStyler(this.tenantList, this.themeService)); // Temporary workaround for https://github.com/microsoft/azuredatastudio/issues/14808 so that we can close an accessibility issue. this.tenantList.getHTMLElement().tabIndex = -1; } protected override layoutBody(size: number): void { this.accountList?.layout(size); this.tenantList?.layout(size); } public get length(): number { return this.accountList!.length; } public override focus() { this.accountList!.domFocus(); } public updateAccounts(accounts: azdata.Account[]) { this.accountList!.splice(0, this.accountList!.length, accounts); } public setSelection(indexes: number[]) { this.accountList!.setSelection(indexes); this.updateTenants(this.accountList!.getSelectedElements()[0]); } private updateTenants(account: azdata.Account) { this.tenantList!.splice(0, this.tenantList!.length, account.properties?.tenants ?? []); } } export interface IProviderViewUiComponent { view: AccountPanel; addAccountAction: AddAccountAction; } export class AccountDialog extends Modal { public static ACCOUNTLIST_HEIGHT = 77; public viewModel: AccountViewModel; // MEMBER VARIABLES //////////////////////////////////////////////////// private _providerViewsMap = new Map(); private _loadingSpinner: LoadingSpinner; private _closeButton?: Button; private _addAccountButton?: Button; private _splitView?: SplitView; private _container?: HTMLElement; private _splitViewContainer?: HTMLElement; private _noaccountViewContainer?: HTMLElement; // EVENTING //////////////////////////////////////////////////////////// private _onAddAccountErrorEmitter: Emitter; public get onAddAccountErrorEvent(): Event { return this._onAddAccountErrorEmitter.event; } private _onCloseEmitter: Emitter; public get onCloseEvent(): Event { return this._onCloseEmitter.event; } constructor( @ILayoutService layoutService: ILayoutService, @IThemeService themeService: IThemeService, @IInstantiationService private _instantiationService: IInstantiationService, @IContextMenuService private _contextMenuService: IContextMenuService, @IKeybindingService private _keybindingService: IKeybindingService, @IConfigurationService private _configurationService: IConfigurationService, @IAdsTelemetryService telemetryService: IAdsTelemetryService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IClipboardService clipboardService: IClipboardService, @ILogService logService: ILogService, @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, @IQuickInputService private _quickInputService: IQuickInputService, @INotificationService private _notificationService: INotificationService, @IOpenerService protected readonly openerService: IOpenerService, @ITelemetryService private readonly vstelemetryService: ITelemetryService, @IAccountManagementService private readonly _accountManagementService: IAccountManagementService, ) { super( localize('linkedAccounts', "Linked accounts"), TelemetryKeys.ModalDialogName.Accounts, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService ); // Setup the event emitters this._onAddAccountErrorEmitter = new Emitter(); this._onCloseEmitter = new Emitter(); // Create the view model and wire up the events this.viewModel = this._instantiationService.createInstance(AccountViewModel); this.viewModel.addProviderEvent(arg => { this.addProvider(arg); }); this.viewModel.removeProviderEvent(arg => { this.removeProvider(arg); }); this.viewModel.updateAccountListEvent(arg => { this.updateProviderAccounts(arg); }); // Load the initial contents of the view model this.viewModel.initialize() .then(addedProviders => { for (const addedProvider of addedProviders) { this.addProvider(addedProvider); } }); } // MODAL OVERRIDE METHODS ////////////////////////////////////////////// protected layout(_height?: number): void { this._splitView!.layout(DOM.getContentHeight(this._container!)); } public override render() { super.render(); attachModalDialogStyler(this, this._themeService); this._closeButton = this.addFooterButton(localize('accountDialog.close', "Close"), () => this.close()); this.registerListeners(); } protected renderBody(container: HTMLElement) { this._container = container; this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true }); this._splitViewContainer = DOM.$('div.account-view.monaco-pane-view'); DOM.append(container, this._splitViewContainer); this._splitView = new SplitView(this._splitViewContainer); this._noaccountViewContainer = DOM.$('div.no-account-view'); const noAccountTitle = DOM.append(this._noaccountViewContainer, DOM.$('.no-account-view-label')); const noAccountLabel = localize('accountDialog.noAccountLabel', "There is no linked account. Please add an account."); noAccountTitle.innerText = noAccountLabel; // Show the add account button for the first provider // Todo: If we have more than 1 provider, need to show all add account buttons for all providers const buttonSection = DOM.append(this._noaccountViewContainer, DOM.$('div.button-section')); this._addAccountButton = new Button(buttonSection); this._addAccountButton.label = localize('accountDialog.addConnection', "Add an account"); this._register(this._addAccountButton.onDidClick(async () => { await this.runAddAccountAction(); })); DOM.append(container, this._noaccountViewContainer); } private registerListeners(): void { // Theme styler this._register(attachButtonStyler(this._closeButton!, this._themeService)); this._register(attachButtonStyler(this._addAccountButton!, this._themeService)); } /* Overwrite escape key behavior */ protected override onClose() { this.close(); } /* Overwrite enter key behavior */ protected override onAccept() { this.close('ok'); } public close(hideReason: HideReason = 'close') { this._onCloseEmitter.fire(); this.hide(hideReason); } public async open() { let accountMetadata = await this._accountManagementService.getAccountProviderMetadata(); this.show(); if (!this.isEmptyLinkedAccount()) { this.showSplitView(); } else if (accountMetadata.length === 0) { this.showLoadingSpinner(); } else { this.showNoAccountContainer(); } } private showNoAccountContainer() { this._loadingSpinner.loading = false; this._splitViewContainer!.hidden = true; this._noaccountViewContainer!.hidden = false; this._addAccountButton!.focus(); } private showLoadingSpinner() { this._loadingSpinner.loadingMessage = localize('accountDialog.loadingProviderLabel', "Loading accounts..."); this._loadingSpinner.loading = true; this._splitViewContainer!.hidden = true; this._noaccountViewContainer!.hidden = true; } private showSplitView() { this._loadingSpinner.loading = false; this._splitViewContainer!.hidden = false; this._noaccountViewContainer!.hidden = true; if (Iterable.consume(this._providerViewsMap.values()).length > 0) { const firstView = this._providerViewsMap.values().next().value; if (firstView && firstView.view instanceof AccountPanel) { firstView.view.setSelection([0]); firstView.view.focus(); } } } private isEmptyLinkedAccount(): boolean { for (const provider of this._providerViewsMap.values()) { const listView = provider.view; if (listView && listView.length > 0) { return false; } } return true; } public override dispose(): void { super.dispose(); for (const provider of this._providerViewsMap.values()) { if (provider.addAccountAction) { provider.addAccountAction.dispose(); } if (provider.view) { provider.view.dispose(); } } } // PRIVATE HELPERS ///////////////////////////////////////////////////// private addProvider(newProvider: AccountProviderAddedEventParams) { this.logService.debug(`Adding new provider ${newProvider.addedProvider.id}`); // Skip adding the provider if it already exists if (this._providerViewsMap.get(newProvider.addedProvider.id)) { this.logService.debug(`Provider ${newProvider.addedProvider.id} is already registered!`); return; } // Account provider doesn't exist, so add it // Create a scoped add account action let addAccountAction = this._instantiationService.createInstance( AddAccountAction, newProvider.addedProvider.id ); addAccountAction.addAccountErrorEvent(msg => this._onAddAccountErrorEmitter.fire(msg)); let providerView = new AccountPanel( { id: newProvider.addedProvider.id, title: newProvider.addedProvider.displayName }, this._keybindingService, this._contextMenuService, this._configurationService, this._themeService, this.contextKeyService, this._instantiationService, this.viewDescriptorService, this.openerService, this.vstelemetryService ); Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ id: newProvider.addedProvider.id, name: newProvider.addedProvider.displayName, ctorDescriptor: new SyncDescriptor(AccountPanel), }], ACCOUNT_VIEW_CONTAINER); this.registerActions(newProvider.addedProvider.id); attachPanelStyler(providerView, this._themeService); const insertIndex = this._splitView!.length; providerView.render(); // Append the list view to the split view this._splitView!.addView(providerView, Sizing.Distribute, insertIndex); providerView.index = insertIndex; this._splitView!.layout(DOM.getContentHeight(this._container!)); // Set the initial items of the list const authLibrary: AuthLibrary = this._configurationService.getValue('azure.authenticationLibrary'); let updatedAccounts: azdata.Account[]; if (authLibrary) { updatedAccounts = filterAccounts(newProvider.initialAccounts, authLibrary); } providerView.updateAccounts(updatedAccounts); if (updatedAccounts.length > 0 && this._splitViewContainer!.hidden) { this.showSplitView(); } this.layout(); this.logService.debug(`Storing view for provider ${newProvider.addedProvider.id}`); // Store the view for the provider and action this._providerViewsMap.set(newProvider.addedProvider.id, { view: providerView, addAccountAction: addAccountAction }); } private removeProvider(removedProvider: azdata.AccountProviderMetadata) { this.logService.debug(`Removing provider ${removedProvider.id}`); // Skip removing the provider if it doesn't exist const providerView = this._providerViewsMap.get(removedProvider.id); if (!providerView || !providerView.view) { this.logService.warn(`Provider ${removedProvider.id} doesn't exist while removing`); return; } // Remove the list view from the split view this._splitView!.removeView(providerView.view.index!); this._splitView!.layout(DOM.getContentHeight(this._container!)); // Remove the list view from our internal map this._providerViewsMap.delete(removedProvider.id); this.logService.debug(`Provider ${removedProvider.id} removed`); this.layout(); } private updateProviderAccounts(args: UpdateAccountListEventParams) { const providerMapping = this._providerViewsMap.get(args.providerId); if (!providerMapping || !providerMapping.view) { return; } const authLibrary: AuthLibrary = this._configurationService.getValue('azure.authenticationLibrary'); let updatedAccounts: azdata.Account[]; if (authLibrary) { updatedAccounts = filterAccounts(args.accountList, authLibrary); } providerMapping.view.updateAccounts(updatedAccounts); if (args.accountList.length > 0 && this._splitViewContainer!.hidden) { this.showSplitView(); } if (this.isEmptyLinkedAccount() && this._noaccountViewContainer!.hidden) { this.showNoAccountContainer(); } this.layout(); } private registerActions(viewId: string) { const that = this; registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.accountDialog.${viewId}.addAccount`, title: { value: localize('accountDialog.addConnection', "Add an account"), original: 'Add an account' }, f1: true, icon: { id: SqlIconId.addAccountAction }, menu: { id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyEqualsExpr.create('view', viewId), order: 1 } }); } async run() { await that.runAddAccountAction(); } }); } private async runAddAccountAction() { this.logService.debug(`Adding account - providers ${JSON.stringify(Iterable.consume(this._providerViewsMap.keys()))}`); const vals = Iterable.consume(this._providerViewsMap.values())[0]; let pickedValue: string | undefined; if (vals.length === 0) { this._notificationService.error(localize('accountDialog.noCloudsRegistered', "You have no clouds enabled. Go to Settings -> Search Azure Account Configuration -> Enable at least one cloud")); return; } if (vals.length > 1) { const buttons: IQuickPickItem[] = vals.map(v => { return { label: v.view.title } as IQuickPickItem; }); const picked = await this._quickInputService.pick(buttons, { canPickMany: false }); pickedValue = picked?.label; } else { pickedValue = vals[0].view.title; } const v = vals.filter(v => v.view.title === pickedValue)?.[0]; if (!v) { this._notificationService.error(localize('accountDialog.didNotPickAuthProvider', "You didn't select any authentication provider. Please try again.")); return; } v.addAccountAction.run(); } } // Filter accounts based on currently selected Auth Library: // if the account key is present, filter based on current auth library // if there is no account key (pre-MSAL account), then it is an ADAL account and // should be displayed as long as ADAL is the currently selected auth library export function filterAccounts(accounts: azdata.Account[], authLibrary: AuthLibrary): azdata.Account[] { let filteredAccounts = accounts.filter(account => { if (account.key.authLibrary) { return account.key.authLibrary === authLibrary; } else { return authLibrary === ADAL_AUTH_LIBRARY; } }); return filteredAccounts; }