diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 3795245c77d..66d2fed76a0 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -18,26 +18,51 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; - -const accountUsages = new Map(); +import { fromNow } from 'vs/base/common/date'; const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git']; -function addAccountUsage(providerId: string, accountName: string, extensionOrFeatureName: string) { - const providerAccountUsage = accountUsages.get(providerId); - if (!providerAccountUsage) { - accountUsages.set(providerId, { [accountName]: [extensionOrFeatureName] }); - } else { - if (providerAccountUsage[accountName]) { - if (!providerAccountUsage[accountName].includes(extensionOrFeatureName)) { - providerAccountUsage[accountName].push(extensionOrFeatureName); - } - } else { - providerAccountUsage[accountName] = [extensionOrFeatureName]; - } +interface IAccountUsage { + extensionId: string; + extensionName: string; + lastUsed: number; +} - accountUsages.set(providerId, providerAccountUsage); +function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { + const accountKey = `${providerId}-${accountName}-usages`; + const storedUsages = storageService.get(accountKey, StorageScope.GLOBAL); + let usages: IAccountUsage[] = []; + if (storedUsages) { + try { + usages = JSON.parse(storedUsages); + } catch (e) { + // ignore + } } + + return usages; +} + +function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) { + const accountKey = `${providerId}-${accountName}-usages`; + const usages = readAccountUsages(storageService, providerId, accountName); + + const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); + if (existingUsageIndex > -1) { + usages.splice(existingUsageIndex, 1, { + extensionId, + extensionName, + lastUsed: Date.now() + }); + } else { + usages.push({ + extensionId, + extensionName, + lastUsed: Date.now() + }); + } + + storageService.store(accountKey, JSON.stringify(usages), StorageScope.GLOBAL); } export class MainThreadAuthenticationProvider extends Disposable { @@ -50,7 +75,8 @@ export class MainThreadAuthenticationProvider extends Disposable { public readonly id: string, public readonly displayName: string, private readonly notificationService: INotificationService, - private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService + private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + private readonly storageService: IStorageService ) { super(); } @@ -64,12 +90,17 @@ export class MainThreadAuthenticationProvider extends Disposable { } private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) { - const quickPick = quickInputService.createQuickPick<{ label: string, extension: AllowedExtension }>(); + const quickPick = 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 items = allowedExtensions.map(extension => { + const usage = usages.find(usage => extension.id === usage.extensionId); return { label: extension.name, + description: usage + ? nls.localize('accountLastUsedDate', "Last used this account {0}", fromNow(usage.lastUsed, true)) + : nls.localize('notUsed', "Has not used this account"), extension }; }); @@ -93,24 +124,6 @@ export class MainThreadAuthenticationProvider extends Disposable { quickPick.show(); } - private showUsage(quickInputService: IQuickInputService, accountName: string) { - const quickPick = quickInputService.createQuickPick(); - const providerUsage = accountUsages.get(this.id); - const accountUsage = (providerUsage || {})[accountName] || []; - - quickPick.items = accountUsage.map(extensionOrFeature => { - return { - label: extensionOrFeature - }; - }); - - quickPick.onDidHide(() => { - quickPick.dispose(); - }); - - quickPick.show(); - } - private async registerCommandsAndContextMenuItems(): Promise { const sessions = await this._proxy.$getSessions(this.id); sessions.forEach(session => this.registerSession(session)); @@ -146,10 +159,9 @@ export class MainThreadAuthenticationProvider extends Disposable { const dialogService = accessor.get(IDialogService); const quickPick = quickInputService.createQuickPick(); - const showUsage = nls.localize('showUsage', "Show Extensions and Features Using This Account"); const manage = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); const signOut = nls.localize('signOut', "Sign Out"); - const items = ([{ label: showUsage }, { label: manage }, { label: signOut }]); + const items = ([{ label: manage }, { label: signOut }]); quickPick.items = items; @@ -163,10 +175,6 @@ export class MainThreadAuthenticationProvider extends Disposable { this.manageTrustedExtensions(quickInputService, storageService, session.account.displayName); } - if (selected.label === showUsage) { - this.showUsage(quickInputService, session.account.displayName); - } - quickPick.dispose(); }); @@ -182,24 +190,21 @@ export class MainThreadAuthenticationProvider extends Disposable { } async signOut(dialogService: IDialogService, session: modes.AuthenticationSession): Promise { - const providerUsage = accountUsages.get(this.id); - const accountUsage = (providerUsage || {})[session.account.displayName] || []; + const accountUsages = readAccountUsages(this.storageService, this.id, session.account.displayName); const sessionsForAccount = this._accounts.get(session.account.displayName); // Skip dialog if nothing is using the account - if (!accountUsage.length) { - accountUsages.set(this.id, { [session.account.displayName]: [] }); + if (!accountUsages.length) { sessionsForAccount?.forEach(sessionId => this.logout(sessionId)); return; } const result = await dialogService.confirm({ title: nls.localize('signOutConfirm', "Sign out of {0}", session.account.displayName), - message: nls.localize('signOutMessage', "The account {0} is currently used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsage.join('\n')) + message: nls.localize('signOutMessage', "The account {0} is has been used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsages.map(usage => usage.extensionName).join('\n')) }); if (result.confirmed) { - accountUsages.set(this.id, { [session.account.displayName]: [] }); sessionsForAccount?.forEach(sessionId => this.logout(sessionId)); } } @@ -211,7 +216,7 @@ export class MainThreadAuthenticationProvider extends Disposable { account: session.account, scopes: session.scopes, getAccessToken: () => { - addAccountUsage(this.id, session.account.displayName, nls.localize('sync', "Preferences Sync")); + addAccountUsage(this.storageService, this.id, session.account.displayName, 'preferencessync', nls.localize('sync', "Preferences Sync")); return this._proxy.$getSessionAccessToken(this.id, session.id); } }; @@ -287,7 +292,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } async $registerAuthenticationProvider(id: string, displayName: string): Promise { - const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, this.notificationService, this.storageKeysSyncRegistryService); + const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, this.notificationService, this.storageKeysSyncRegistryService, this.storageService); await provider.initialize(); this.authenticationService.registerAuthenticationProvider(id, provider); } @@ -374,16 +379,16 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { - addAccountUsage(providerId, accountName, extensionName); - const allowList = readAllowedExtensions(this.storageService, providerId, accountName); const extensionData = allowList.find(extension => extension.id === extensionId); if (extensionData) { + addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); return true; } const remoteConnection = this.remoteAgentService.getConnection(); if (remoteConnection && remoteConnection.remoteAuthority && remoteConnection.remoteAuthority.startsWith('vsonline') && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { + addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); return true; } @@ -398,6 +403,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu const allow = choice === 0; if (allow) { + addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); allowList.push({ id: extensionId, name: extensionName }); this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL); }