Skip to content

Commit

Permalink
Have account menu be snappier by caching accounts
Browse files Browse the repository at this point in the history
Before, we were calling out to the auth providers (thus going all the way to the extension host per provider)

Now, we cache the accounts and handle events for when accounts change.

This makes the account menu way snappier!

Fixes #111398
  • Loading branch information
TylerLeonhardt committed Mar 6, 2023
1 parent 6aada10 commit 80c81ba
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 49 deletions.
213 changes: 168 additions & 45 deletions src/vs/workbench/browser/parts/activitybar/activitybarActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { getCurrentAuthenticationSessionInfo } from 'vs/workbench/services/authentication/browser/authenticationService';
import { AuthenticationSession, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
import { AuthenticationSessionAccount, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProductService } from 'vs/platform/product/common/productService';
Expand All @@ -40,6 +40,7 @@ import { IPaneCompositePart } from 'vs/workbench/browser/parts/paneCompositePart
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { ILogService } from 'vs/platform/log/common/log';

export class ViewContainerActivityAction extends ActivityAction {

Expand Down Expand Up @@ -230,6 +231,12 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {

static readonly ACCOUNTS_VISIBILITY_PREFERENCE_KEY = 'workbench.activity.showAccounts';

private readonly groupedAccounts: Map<string, (AuthenticationSessionAccount & { canSignOut: boolean })[]> = new Map();
private readonly problematicProviders: Set<string> = new Set();

private initialized = false;
private sessionFromEmbedder = getCurrentAuthenticationSessionInfo(this.credentialsService, this.productService);

constructor(
action: ActivityAction,
contextMenuActionsProvider: () => IAction[],
Expand All @@ -247,65 +254,110 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
@IStorageService private readonly storageService: IStorageService,
@IKeybindingService keybindingService: IKeybindingService,
@ICredentialsService private readonly credentialsService: ICredentialsService,
@ILogService private readonly logService: ILogService
) {
super(MenuId.AccountsContext, action, contextMenuActionsProvider, true, colors, activityHoverOptions, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService);
this.registerListeners();
this.initialize();
}

private registerListeners(): void {
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async (e) => {
await this.addAccountsFromProvider(e.id);
}));

this._register(this.authenticationService.onDidUnregisterAuthenticationProvider((e) => {
this.groupedAccounts.delete(e.id);
this.problematicProviders.delete(e.id);
}));

this._register(this.authenticationService.onDidChangeSessions(async e => {
const promises = [];
for (const changed of e.event.changed) {
promises.push(this.addOrUpdateAccount(e.providerId, changed.account));
}
for (const added of e.event.added) {
promises.push(this.addOrUpdateAccount(e.providerId, added.account));
}
const result = await Promise.allSettled(promises);
for (const r of result) {
if (r.status === 'rejected') {
this.logService.error(r.reason);
}
}
for (const removed of e.event.removed) {
this.removeAccount(e.providerId, removed.account);
}
}));
}

// This function exists to ensure that the accounts are added for auth providers that had already been registered
// before the menu was created.
private async initialize(): Promise<void> {
const providerIds = this.authenticationService.getProviderIds();
const results = await Promise.allSettled(providerIds.map(providerId => this.addAccountsFromProvider(providerId)));

// Log any errors that occurred while initializing. We try to be best effort here to show the most amount of accounts
for (const result of results) {
if (result.status === 'rejected') {
this.logService.error(result.reason);
}
}

this.initialized = true;
}

//#region overrides

protected override async resolveMainMenuActions(accountsMenu: IMenu, disposables: DisposableStore): Promise<IAction[]> {
await super.resolveMainMenuActions(accountsMenu, disposables);

const otherCommands = accountsMenu.getActions();
const providers = this.authenticationService.getProviderIds();
const allSessions = providers.map(async providerId => {
try {
const sessions = await this.authenticationService.getSessions(providerId);

const groupedSessions: { [label: string]: AuthenticationSession[] } = {};
sessions.forEach(session => {
if (groupedSessions[session.account.label]) {
groupedSessions[session.account.label].push(session);
} else {
groupedSessions[session.account.label] = [session];
}
});
const otherCommands = accountsMenu.getActions();
let menus: IAction[] = [];

return { providerId, sessions: groupedSessions };
} catch {
return { providerId };
for (const providerId of providers) {
if (!this.initialized) {
const noAccountsAvailableAction = disposables.add(new Action('noAccountsAvailable', localize('loading', "Loading..."), undefined, false));
menus.push(noAccountsAvailableAction);
break;
}
const providerLabel = this.authenticationService.getLabel(providerId);
const accounts = this.groupedAccounts.get(providerId);
if (!accounts) {
if (this.problematicProviders.has(providerId)) {
const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', providerLabel), undefined, false));
menus.push(providerUnavailableAction);
// try again in the background so that if the failure was intermittent, we can resolve it on the next showing of the menu
try {
await this.addAccountsFromProvider(providerId);
} catch (e) {
this.logService.error(e);
}
}
continue;
}
});

const result = await Promise.all(allSessions);
let menus: IAction[] = [];
const authenticationSession = await getCurrentAuthenticationSessionInfo(this.credentialsService, this.productService);
result.forEach(sessionInfo => {
const providerDisplayName = this.authenticationService.getLabel(sessionInfo.providerId);

if (sessionInfo.sessions) {
Object.keys(sessionInfo.sessions).forEach(accountName => {
const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => {
return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName);
}));

const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), '', true, () => {
return this.authenticationService.removeAccountSessions(sessionInfo.providerId, accountName, sessionInfo.sessions[accountName]);
}));
for (const account of accounts) {
const manageExtensionsAction = disposables.add(new Action(`configureSessions${account.label}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), undefined, true, () => {
return this.authenticationService.manageTrustedExtensionsForAccount(providerId, account.label);
}));

const providerSubMenuActions = [manageExtensionsAction];
const providerSubMenuActions: Action[] = [manageExtensionsAction];

const hasEmbedderAccountSession = sessionInfo.sessions[accountName].some(session => session.id === (authenticationSession?.id));
if (!hasEmbedderAccountSession || authenticationSession?.canSignOut) {
providerSubMenuActions.push(signOutAction);
}
if (account.canSignOut) {
const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), undefined, true, async () => {
const allSessions = await this.authenticationService.getSessions(providerId);
const sessionsForAccount = allSessions.filter(s => s.account.id === account.id);
return await this.authenticationService.removeAccountSessions(providerId, account.label, sessionsForAccount);
}));
providerSubMenuActions.push(signOutAction);
}

const providerSubMenu = new SubmenuAction('activitybar.submenu', `${accountName} (${providerDisplayName})`, providerSubMenuActions);
menus.push(providerSubMenu);
});
} else {
const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName)));
menus.push(providerUnavailableAction);
const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${providerLabel})`, providerSubMenuActions);
menus.push(providerSubMenu);
}
});
}

if (providers.length && !menus.length) {
const noAccountsAvailableAction = disposables.add(new Action('noAccountsAvailable', localize('noAccounts', "You are not signed in to any accounts"), undefined, false));
Expand Down Expand Up @@ -337,6 +389,77 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {

return actions;
}

//#endregion

//#region groupedAccounts helpers

private async addOrUpdateAccount(providerId: string, account: AuthenticationSessionAccount): Promise<void> {
const accounts = this.groupedAccounts.get(providerId);
const existingAccount = accounts?.find(a => a.id === account.id);
if (existingAccount) {
if (existingAccount.label !== account.label) {
existingAccount.label = account.label;
}
return;
}

const sessionFromEmbedder = await this.sessionFromEmbedder;
// If the session stored from the embedder allows sign out, then we can treat it and all others as sign out-able
let canSignOut = !!sessionFromEmbedder?.canSignOut;
if (!canSignOut) {
if (sessionFromEmbedder?.id) {
const sessions = (await this.authenticationService.getSessions(providerId)).filter(s => s.account.id === account.id);
canSignOut = !sessions.some(s => s.id === sessionFromEmbedder.id);
} else {
// The default if we don't have a session from the embedder is to allow sign out
canSignOut = true;
}
}

if (!accounts) {
this.groupedAccounts.set(providerId, [{ ...account, canSignOut }]);
return;
}

accounts.push({ ...account, canSignOut });
}

private removeAccount(providerId: string, account: AuthenticationSessionAccount): void {
const accounts = this.groupedAccounts.get(providerId);
if (!accounts) {
return;
}

const index = accounts.findIndex(a => a.id === account.id);
if (index === -1) {
return;
}

accounts.splice(index, 1);
if (accounts.length === 0) {
this.groupedAccounts.delete(providerId);
}
}

private async addAccountsFromProvider(providerId: string): Promise<void> {
try {
const sessions = await this.authenticationService.getSessions(providerId);
this.problematicProviders.delete(providerId);

const result = await Promise.allSettled(sessions.map(s => this.addOrUpdateAccount(providerId, s.account)));
for (const r of result) {
if (r.status === 'rejected') {
this.logService.error(r.reason);
}
}
} catch (e) {
this.logService.error(e);
this.problematicProviders.add(providerId);
}
}

//#endregion
}

export interface IProfileActivity extends IActivity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';

export interface AuthenticationSessionAccount {
label: string;
id: string;
}

export interface AuthenticationSession {
id: string;
accessToken: string;
account: {
label: string;
id: string;
};
account: AuthenticationSessionAccount;
scopes: ReadonlyArray<string>;
idToken?: string;
}
Expand Down

0 comments on commit 80c81ba

Please sign in to comment.