Skip to content

Commit

Permalink
Apply left bottom menu actions
Browse files Browse the repository at this point in the history
  • Loading branch information
vinokurig committed Aug 31, 2020
1 parent 90eb96b commit 9c36df8
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 4 deletions.
186 changes: 185 additions & 1 deletion packages/core/src/browser/authentication-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@
*--------------------------------------------------------------------------------------------*/
// code copied and modified from https://github.com/microsoft/vscode/blob/1.47.3/src/vs/workbench/services/authentication/browser/authenticationService.ts

import { injectable } from 'inversify';
import { injectable, inject, postConstruct } from 'inversify';
import { Emitter, Event } from '../common/event';
import { StorageService } from '../browser/storage-service';
import { Disposable } from '../common/disposable';
import { ACCOUNTS_MENU, ACCOUNTS_SUBMENU, MenuModelRegistry } from '../common/menu';
import { CommandRegistry } from '../common/command';
import { DisposableCollection } from '../common/disposable';

export interface AuthenticationSession {
id: string;
Expand All @@ -45,6 +49,15 @@ export interface AuthenticationProviderInformation {
label: string;
}

export interface SessionRequest {
disposables: Disposable[];
requestingExtensionIds: string[];
}

export interface SessionRequestInfo {
[scopes: string]: SessionRequest;
}

export interface AuthenticationProvider {
id: string;

Expand All @@ -71,6 +84,7 @@ export interface AuthenticationService {
getProviderIds(): string[];
registerAuthenticationProvider(id: string, provider: AuthenticationProvider): void;
unregisterAuthenticationProvider(id: string): void;
requestNewSession(id: string, scopes: string[], extensionId: string, extensionName: string): void;
sessionsUpdate(providerId: string, event: AuthenticationSessionsChangeEvent): void;

readonly onDidRegisterAuthenticationProvider: Event<AuthenticationProviderInformation>;
Expand All @@ -88,6 +102,9 @@ export interface AuthenticationService {

@injectable()
export class AuthenticationServiceImpl implements AuthenticationService {
private noAccountsMenuItem: Disposable | undefined;
private signInRequestItems = new Map<string, SessionRequestInfo>();

private authenticationProviders: Map<string, AuthenticationProvider> = new Map<string, AuthenticationProvider>();

private onDidRegisterAuthenticationProviderEmitter: Emitter<AuthenticationProviderInformation> = new Emitter<AuthenticationProviderInformation>();
Expand All @@ -100,6 +117,53 @@ export class AuthenticationServiceImpl implements AuthenticationService {
new Emitter<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }>();
readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }> = this.onDidChangeSessionsEmitter.event;

@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry;
@inject(CommandRegistry) protected readonly commands: CommandRegistry;
@inject(StorageService) protected readonly storageService: StorageService;

@postConstruct()
init(): void {
const disposableMap = new Map<string, DisposableCollection>();
this.onDidChangeSessions(async e => {
if (e.event.added.length > 0) {
const sessions = await this.getSessions(e.providerId);
sessions.forEach(session => {
const disposables = new DisposableCollection();
const commandId = `account-sign-out-${e.providerId}-${session.id}`;
const command = this.commands.registerCommand({ id: commandId }, {
execute: async () => {
this.signOutOfAccount(e.providerId, session.account.label);
}
});
const subSubMenuPath = [...ACCOUNTS_SUBMENU, 'account-sub-menu'];
this.menus.registerSubmenu(subSubMenuPath, `${session.account.label} (${e.label})`);
const menuAction = this.menus.registerMenuAction(subSubMenuPath, {
label: 'Sign Out',
commandId
});
disposables.push(menuAction);
disposables.push(command);
disposableMap.set(session.id, disposables);
});
}
if (e.event.removed.length > 0) {
e.event.removed.forEach(removed => {
const toDispose = disposableMap.get(removed);
if (toDispose) {
toDispose.dispose();
disposableMap.delete(removed);
}
});
}
});
this.commands.registerCommand({ id: 'noAccounts'}, {
execute: async () => {},
isEnabled(): boolean {
return false;
}
});
}

getProviderIds(): string[] {
const providerIds: string[] = [];
this.authenticationProviders.forEach(provider => {
Expand All @@ -112,16 +176,39 @@ export class AuthenticationServiceImpl implements AuthenticationService {
return this.authenticationProviders.has(id);
}

private updateAccountsMenuItem(): void {
let hasSession = false;
this.authenticationProviders.forEach(async provider => {
hasSession = hasSession || provider.hasSessions();
});

if (hasSession && this.noAccountsMenuItem) {
this.noAccountsMenuItem.dispose();
this.noAccountsMenuItem = undefined;
}

if (!hasSession && !this.noAccountsMenuItem) {
this.noAccountsMenuItem = this.menus.registerMenuAction(ACCOUNTS_MENU, {
label: 'You are not signed in to any accounts',
order: '0',
commandId: 'noAccounts'
});
}
}

registerAuthenticationProvider(id: string, authenticationProvider: AuthenticationProvider): void {
this.authenticationProviders.set(id, authenticationProvider);
this.onDidRegisterAuthenticationProviderEmitter.fire({ id, label: authenticationProvider.label });

this.updateAccountsMenuItem();
}

unregisterAuthenticationProvider(id: string): void {
const provider = this.authenticationProviders.get(id);
if (provider) {
this.authenticationProviders.delete(id);
this.onDidUnregisterAuthenticationProviderEmitter.fire({ id, label: provider.label });
this.updateAccountsMenuItem();
}
}

Expand All @@ -130,6 +217,103 @@ export class AuthenticationServiceImpl implements AuthenticationService {
if (provider) {
await provider.updateSessionItems(event);
this.onDidChangeSessionsEmitter.fire({ providerId: id, label: provider.label, event: event });
this.updateAccountsMenuItem();

if (event.added) {
await this.updateNewSessionRequests(provider);
}
}
}

private async updateNewSessionRequests(provider: AuthenticationProvider): Promise<void> {
const existingRequestsForProvider = this.signInRequestItems.get(provider.id);
if (!existingRequestsForProvider) {
return;
}

const sessions = await provider.getSessions();
Object.keys(existingRequestsForProvider).forEach(requestedScopes => {
if (sessions.some(session => session.scopes.slice().sort().join('') === requestedScopes)) {
const sessionRequest = existingRequestsForProvider[requestedScopes];
if (sessionRequest) {
sessionRequest.disposables.forEach(item => item.dispose());
}

delete existingRequestsForProvider[requestedScopes];
if (Object.keys(existingRequestsForProvider).length === 0) {
this.signInRequestItems.delete(provider.id);
} else {
this.signInRequestItems.set(provider.id, existingRequestsForProvider);
}
}
});
}

async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise<void> {
let provider = this.authenticationProviders.get(providerId);
if (!provider) {
// Activate has already been called for the authentication provider, but it cannot block on registering itself
// since this is sync and returns a disposable. So, wait for registration event to fire that indicates the
// provider is now in the map.
await new Promise((resolve, _) => {
this.onDidRegisterAuthenticationProvider(e => {
if (e.id === providerId) {
provider = this.authenticationProviders.get(providerId);
resolve();
}
});
});
}

if (provider) {
const providerRequests = this.signInRequestItems.get(providerId);
const scopesList = scopes.sort().join('');
const extensionHasExistingRequest = providerRequests
&& providerRequests[scopesList]
&& providerRequests[scopesList].requestingExtensionIds.indexOf(extensionId) > 0;

if (extensionHasExistingRequest) {
return;
}

const menuItem = this.menus.registerMenuAction(ACCOUNTS_SUBMENU, {
label: `Sign in to use ${extensionName} (1)`,
order: '1',
commandId: `${extensionId}signIn`,
});

const signInCommand = this.commands.registerCommand({ id: `${extensionId}signIn`}, {
execute: async () => {
const session = await this.login(providerId, scopes);

// Add extension to allow list since user explicitly signed in on behalf of it
const allowList = await readAllowedExtensions(this.storageService, providerId, session.account.label);
if (!allowList.find(allowed => allowed.id === extensionId)) {
allowList.push({ id: extensionId, name: extensionName });
this.storageService.setData(`${providerId}-${session.account.label}`, JSON.stringify(allowList));
}

// And also set it as the preferred account for the extension
this.storageService.setData(`${extensionName}-${providerId}`, session.id);
}
});

if (providerRequests) {
const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] };

providerRequests[scopesList] = {
disposables: [...existingRequest.disposables, menuItem, signInCommand],
requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId]
};
this.signInRequestItems.set(providerId, providerRequests);
} else {
this.signInRequestItems.set(providerId, {
[scopesList]: {
disposables: [menuItem, signInCommand],
requestingExtensionIds: [extensionId]
}
});
}
}
}

Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import debounce = require('lodash.debounce');
import { injectable, inject } from 'inversify';
import { TabBar, Widget, Title } from '@phosphor/widgets';
import { MAIN_MENU_BAR, SETTINGS_MENU, MenuContribution, MenuModelRegistry } from '../common/menu';
import { MAIN_MENU_BAR, SETTINGS_MENU, MenuContribution, MenuModelRegistry, ACCOUNTS_MENU } from '../common/menu';
import { KeybindingContribution, KeybindingRegistry } from './keybinding';
import { FrontendApplication, FrontendApplicationContribution } from './frontend-application';
import { CommandContribution, CommandRegistry, Command } from '../common/command';
Expand Down Expand Up @@ -362,6 +362,13 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
menuPath: SETTINGS_MENU,
order: 0,
});
app.shell.leftPanelHandler.addMenu({
id: 'accounts-menu',
iconClass: 'codicon codicon-person',
title: 'Accounts',
menuPath: ACCOUNTS_MENU,
order: 1,
});
}

protected updateStyles(): void {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/common/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export type MenuPath = string[];
export const MAIN_MENU_BAR: MenuPath = ['menubar'];

export const SETTINGS_MENU: MenuPath = ['settings_menu'];
export const ACCOUNTS_MENU: MenuPath = ['accounts_menu'];
export const ACCOUNTS_SUBMENU = [...ACCOUNTS_MENU, '1_accounts_submenu'];

export const MenuContribution = Symbol('MenuContribution');

Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,7 @@ export interface AuthenticationMain {
$getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
$loginPrompt(providerName: string, extensionName: string): Promise<boolean>;
$setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise<void>;
$requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise<void>;

$getSessions(providerId: string): Promise<ReadonlyArray<theia.AuthenticationSession>>;
$login(providerId: string, scopes: string[]): Promise<theia.AuthenticationSession>;
Expand Down
11 changes: 9 additions & 2 deletions packages/plugin-ext/src/main/browser/authentication-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export class AuthenticationMainImpl implements AuthenticationMain {
return this.authenticationService.logout(providerId, sessionId);
}

async $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise<void> {
return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName);
}

async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string,
options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise<AuthenticationSession | undefined> {
const orderedScopes = scopes.sort().join(' ');
Expand Down Expand Up @@ -116,6 +120,9 @@ export class AuthenticationMainImpl implements AuthenticationMain {
const session = await this.authenticationService.login(providerId, scopes);
await this.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
return session;
} else {
await this.$requestNewSession(providerId, scopes, extensionId, extensionName);
return undefined;
}
}
}
Expand Down Expand Up @@ -269,8 +276,8 @@ export class AuthenticationProviderImp implements AuthenticationProvider {
const accountUsages = await readAccountUsages(this.storageService, this.id, accountName);
const sessionsForAccount = this.accounts.get(accountName);

const result = await this.messageService.info(`The account ${accountName} has been used by: \\n\\n
${accountUsages.map(usage => usage.extensionName).join('\n')}\\n\\n Sign out of these features?`, 'Yes');
const result = await this.messageService.info(`The account ${accountName} has been used by:
${accountUsages.map(usage => usage.extensionName).join(', ')}. Sign out of these features?`, 'Yes');

if (result && result === 'Yes' && sessionsForAccount) {
sessionsForAccount.forEach(sessionId => this.logout(sessionId));
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-ext/src/plugin/authentication-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export class AuthenticationExtImpl implements AuthenticationExt {
const session = await provider.login(scopes);
await this.proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
return session;
} else {
await this.proxy.$requestNewSession(providerId, scopes, extensionId, extensionName);
return undefined;
}
}
}
Expand Down

0 comments on commit 9c36df8

Please sign in to comment.