diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 3c0e9eaa4c6..96ed5742429 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import * as modes from 'vs/editor/common/modes'; import * as nls from 'vs/nls'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; @@ -12,8 +12,6 @@ import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContex import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import Severity from 'vs/base/common/severity'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; @@ -71,7 +69,6 @@ function addAccountUsage(storageService: IStorageService, providerId: string, ac } export class MainThreadAuthenticationProvider extends Disposable { - private _sessionMenuItems = new Map(); private _accounts = new Map(); // Map account name to session ids private _sessions = new Map(); // Map account id to name @@ -82,7 +79,9 @@ export class MainThreadAuthenticationProvider extends Disposable { public readonly supportsMultipleAccounts: boolean, private readonly notificationService: INotificationService, private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, - private readonly storageService: IStorageService + private readonly storageService: IStorageService, + private readonly quickInputService: IQuickInputService, + private readonly dialogService: IDialogService ) { super(); } @@ -95,11 +94,11 @@ export class MainThreadAuthenticationProvider extends Disposable { return !!this._sessions.size; } - private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) { - const quickPick = quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>(); + public manageTrustedExtensions(accountName: string) { + const quickPick = this.quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>(); quickPick.canSelectMany = true; - const allowedExtensions = readAllowedExtensions(storageService, this.id, accountName); - const usages = readAccountUsages(storageService, this.id, accountName); + const allowedExtensions = readAllowedExtensions(this.storageService, this.id, accountName); + const usages = readAccountUsages(this.storageService, this.id, accountName); const items = allowedExtensions.map(extension => { const usage = usages.find(usage => extension.id === usage.extensionId); return { @@ -118,7 +117,7 @@ export class MainThreadAuthenticationProvider extends Disposable { quickPick.onDidAccept(() => { const updatedAllowedList = quickPick.selectedItems.map(item => item.extension); - storageService.store(`${this.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.GLOBAL); + this.storageService.store(`${this.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.GLOBAL); quickPick.dispose(); }); @@ -146,69 +145,23 @@ export class MainThreadAuthenticationProvider extends Disposable { this._accounts.set(session.account.displayName, [session.id]); } - const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '1_accounts', - command: { - id: `configureSessions${session.id}`, - title: `${session.account.displayName} (${this.displayName})` - }, - order: 3 - }); - this.storageKeysSyncRegistryService.registerStorageKey({ key: `${this.id}-${session.account.displayName}`, version: 1 }); - - const manageCommand = CommandsRegistry.registerCommand({ - id: `configureSessions${session.id}`, - handler: (accessor, args) => { - const quickInputService = accessor.get(IQuickInputService); - const storageService = accessor.get(IStorageService); - const dialogService = accessor.get(IDialogService); - - const quickPick = quickInputService.createQuickPick(); - const manage = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); - const signOut = nls.localize('signOut', "Sign Out"); - const items = ([{ label: manage }, { label: signOut }]); - - quickPick.items = items; - - quickPick.onDidAccept(e => { - const selected = quickPick.selectedItems[0]; - if (selected.label === signOut) { - this.signOut(dialogService, session); - } - - if (selected.label === manage) { - this.manageTrustedExtensions(quickInputService, storageService, session.account.displayName); - } - - quickPick.dispose(); - }); - - quickPick.onDidHide(_ => { - quickPick.dispose(); - }); - - quickPick.show(); - }, - }); - - this._sessionMenuItems.set(session.account.displayName, [menuItem, manageCommand]); } - async signOut(dialogService: IDialogService, session: modes.AuthenticationSession): Promise { - const accountUsages = readAccountUsages(this.storageService, this.id, session.account.displayName); - const sessionsForAccount = this._accounts.get(session.account.displayName); + async signOut(accountName: string): Promise { + const accountUsages = readAccountUsages(this.storageService, this.id, accountName); + const sessionsForAccount = this._accounts.get(accountName); - const result = await dialogService.confirm({ - title: nls.localize('signOutConfirm', "Sign out of {0}", session.account.displayName), + const result = await this.dialogService.confirm({ + title: nls.localize('signOutConfirm', "Sign out of {0}", accountName), message: accountUsages.length - ? nls.localize('signOutMessagve', "The account {0} has been used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsages.map(usage => usage.extensionName).join('\n')) - : nls.localize('signOutMessageSimple', "Sign out of {0}?", session.account.displayName) + ? nls.localize('signOutMessagve', "The account {0} has been used by: \n\n{1}\n\n Sign out of these features?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) + : nls.localize('signOutMessageSimple', "Sign out of {0}?", accountName) }); if (result.confirmed) { sessionsForAccount?.forEach(sessionId => this.logout(sessionId)); - removeAccountUsage(this.storageService, this.id, session.account.displayName); + removeAccountUsage(this.storageService, this.id, accountName); } } @@ -230,11 +183,6 @@ export class MainThreadAuthenticationProvider extends Disposable { sessionsForAccount.splice(sessionIndex); if (!sessionsForAccount.length) { - const disposeables = this._sessionMenuItems.get(accountName); - if (disposeables) { - disposeables.forEach(disposeable => disposeable.dispose()); - this._sessionMenuItems.delete(accountName); - } this._accounts.delete(accountName); } } @@ -251,12 +199,6 @@ export class MainThreadAuthenticationProvider extends Disposable { await this._proxy.$logout(this.id, sessionId); this.notificationService.info(nls.localize('signedOut', "Successfully signed out.")); } - - dispose(): void { - super.dispose(); - this._sessionMenuItems.forEach(item => item.forEach(d => d.dispose())); - this._sessionMenuItems.clear(); - } } @extHostNamedCustomer(MainContext.MainThreadAuthentication) @@ -294,7 +236,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } async $registerAuthenticationProvider(id: string, displayName: string, supportsMultipleAccounts: boolean): Promise { - const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, supportsMultipleAccounts, this.notificationService, this.storageKeysSyncRegistryService, this.storageService); + const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, supportsMultipleAccounts, this.notificationService, this.storageKeysSyncRegistryService, this.storageService, this.quickInputService, this.dialogService); await provider.initialize(); this.authenticationService.registerAuthenticationProvider(id, provider); } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index e4b3454fa27..0606c532f5a 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -11,7 +11,7 @@ import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch import { Action, IAction } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose } from 'vs/base/common/lifecycle'; -import { SyncActionDescriptor, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -28,8 +28,11 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Codicon } from 'vs/base/common/codicons'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionViewItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { isMacintosh } from 'vs/base/common/platform'; +import { ContextSubMenu } from 'vs/base/browser/contextmenu'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { distinct } from 'vs/base/common/arrays'; export class ViewContainerActivityAction extends ActivityAction { @@ -103,6 +106,7 @@ export class AccountsActionViewItem extends ActivityActionViewItem { @IContextMenuService protected contextMenuService: IContextMenuService, @IMenuService protected menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService ) { super(action, { draggable: false, colors, icon: true }, themeService); } @@ -132,16 +136,60 @@ export class AccountsActionViewItem extends ActivityActionViewItem { })); } - private showContextMenu(): void { + private async getActions(accountsMenu: IMenu) { + const otherCommands = accountsMenu.getActions(); + const providers = this.authenticationService.getProviderIds(); + const allSessions = providers.map(async id => { + const sessions = await this.authenticationService.getSessions(id); + const uniqueSessions = distinct(sessions, session => session.account.displayName); + return { + providerId: id, + sessions: uniqueSessions + }; + }); + + const result = await Promise.all(allSessions); + let menus: (IAction | ContextSubMenu)[] = []; + result.forEach(sessionInfo => { + const providerDisplayName = this.authenticationService.getDisplayName(sessionInfo.providerId); + sessionInfo.sessions.forEach(session => { + const accountName = session.account.displayName; + const menu = new ContextSubMenu(`${accountName} (${providerDisplayName})`, [ + new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, _ => { + return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); + }), + new Action('signOut', nls.localize('signOut', "Sign Out"), '', true, _ => { + return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName); + }) + ]); + menus.push(menu); + }); + }); + + if (menus.length) { + menus.push(new Separator()); + } + + otherCommands.forEach(group => { + const actions = group[1]; + menus = menus.concat(actions); + menus.push(new Separator()); + }); + + return menus; + } + + private async showContextMenu(): Promise { const accountsActions: IAction[] = []; const accountsMenu = this.menuService.createMenu(MenuId.AccountsContext, this.contextKeyService); const actionsDisposable = createAndFillInActionBarActions(accountsMenu, undefined, { primary: [], secondary: accountsActions }); const containerPosition = DOM.getDomNodePagePosition(this.container); const location = { x: containerPosition.left + containerPosition.width / 2, y: containerPosition.top }; + const actions = await this.getActions(accountsMenu); this.contextMenuService.showContextMenu({ getAnchor: () => location, - getActions: () => accountsActions, + getActions: () => actions, onHide: () => { accountsMenu.dispose(); dispose(actionsDisposable); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index c43a7e7078e..8641ab6d7ba 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -37,6 +37,9 @@ export interface IAuthenticationService { supportsMultipleAccounts(providerId: string): boolean; login(providerId: string, scopes: string[]): Promise; logout(providerId: string, sessionId: string): Promise; + + manageTrustedExtensionsForAccount(providerId: string, accountName: string): Promise; + signOutOfAccount(providerId: string, accountName: string): Promise; } export interface AllowedExtension { @@ -330,6 +333,24 @@ export class AuthenticationService extends Disposable implements IAuthentication throw new Error(`No authentication provider '${id}' is currently registered.`); } } + + async manageTrustedExtensionsForAccount(id: string, accountName: string): Promise { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return authProvider.manageTrustedExtensions(accountName); + } else { + throw new Error(`No authentication provider '${id}' is currently registered.`); + } + } + + async signOutOfAccount(id: string, accountName: string): Promise { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return authProvider.signOut(accountName); + } else { + throw new Error(`No authentication provider '${id}' is currently registered.`); + } + } } registerSingleton(IAuthenticationService, AuthenticationService);