mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
Add account context menu, #90385
This commit is contained in:
parent
aba05ec2b4
commit
2b86488f03
|
@ -349,6 +349,10 @@
|
|||
{
|
||||
"name": "vs/workbench/contrib/timeline",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/services/authentication",
|
||||
"project": "vscode-workbench"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { keychain } from './common/keychain';
|
|||
import { GitHubServer } from './githubServer';
|
||||
import Logger from './common/logger';
|
||||
|
||||
export const onDidChangeSessions = new vscode.EventEmitter<void>();
|
||||
export const onDidChangeSessions = new vscode.EventEmitter<vscode.AuthenticationSessionsChangeEvent>();
|
||||
|
||||
interface SessionData {
|
||||
id: string;
|
||||
|
@ -29,14 +29,16 @@ export class GitHubAuthenticationProvider {
|
|||
private pollForChange() {
|
||||
setTimeout(async () => {
|
||||
const storedSessions = await this.readSessions();
|
||||
let didChange = false;
|
||||
|
||||
const added: string[] = [];
|
||||
const removed: string[] = [];
|
||||
|
||||
storedSessions.forEach(session => {
|
||||
const matchesExisting = this._sessions.some(s => s.id === session.id);
|
||||
// Another window added a session to the keychain, add it to our state as well
|
||||
if (!matchesExisting) {
|
||||
this._sessions.push(session);
|
||||
didChange = true;
|
||||
added.push(session.id);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -49,12 +51,12 @@ export class GitHubAuthenticationProvider {
|
|||
this._sessions.splice(sessionIndex, 1);
|
||||
}
|
||||
|
||||
didChange = true;
|
||||
removed.push(session.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (didChange) {
|
||||
onDidChangeSessions.fire();
|
||||
if (added.length || removed.length) {
|
||||
onDidChangeSessions.fire({ added, removed, changed: [] });
|
||||
}
|
||||
|
||||
this.pollForChange();
|
||||
|
|
|
@ -54,7 +54,7 @@ function parseQuery(uri: vscode.Uri) {
|
|||
}, {});
|
||||
}
|
||||
|
||||
export const onDidChangeSessions = new vscode.EventEmitter<void>();
|
||||
export const onDidChangeSessions = new vscode.EventEmitter<vscode.AuthenticationSessionsChangeEvent>();
|
||||
|
||||
export const REFRESH_NETWORK_FAILURE = 'Network failure';
|
||||
|
||||
|
@ -129,7 +129,8 @@ export class AzureActiveDirectoryService {
|
|||
|
||||
private pollForChange() {
|
||||
setTimeout(async () => {
|
||||
let didChange = false;
|
||||
const addedIds: string[] = [];
|
||||
let removedIds: string[] = [];
|
||||
const storedData = await keychain.getToken();
|
||||
if (storedData) {
|
||||
try {
|
||||
|
@ -139,7 +140,7 @@ export class AzureActiveDirectoryService {
|
|||
if (!matchesExisting) {
|
||||
try {
|
||||
await this.refreshToken(session.refreshToken, session.scope);
|
||||
didChange = true;
|
||||
addedIds.push(session.id);
|
||||
} catch (e) {
|
||||
if (e.message === REFRESH_NETWORK_FAILURE) {
|
||||
// Ignore, will automatically retry on next poll.
|
||||
|
@ -154,7 +155,7 @@ export class AzureActiveDirectoryService {
|
|||
const matchesExisting = sessions.some(session => token.scope === session.scope && token.sessionId === session.id);
|
||||
if (!matchesExisting) {
|
||||
await this.logout(token.sessionId);
|
||||
didChange = true;
|
||||
removedIds.push(token.sessionId);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -162,19 +163,19 @@ export class AzureActiveDirectoryService {
|
|||
} catch (e) {
|
||||
Logger.error(e.message);
|
||||
// if data is improperly formatted, remove all of it and send change event
|
||||
removedIds = this._tokens.map(token => token.sessionId);
|
||||
this.clearSessions();
|
||||
didChange = true;
|
||||
}
|
||||
} else {
|
||||
if (this._tokens.length) {
|
||||
// Log out all
|
||||
removedIds = this._tokens.map(token => token.sessionId);
|
||||
await this.clearSessions();
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (didChange) {
|
||||
onDidChangeSessions.fire();
|
||||
if (addedIds.length || removedIds.length) {
|
||||
onDidChangeSessions.fire({ added: addedIds, removed: removedIds, changed: [] });
|
||||
}
|
||||
|
||||
this.pollForChange();
|
||||
|
@ -377,7 +378,7 @@ export class AzureActiveDirectoryService {
|
|||
this._refreshTimeouts.set(token.sessionId, setTimeout(async () => {
|
||||
try {
|
||||
await this.refreshToken(token.refreshToken, scope);
|
||||
onDidChangeSessions.fire();
|
||||
onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] });
|
||||
} catch (e) {
|
||||
if (e.message === REFRESH_NETWORK_FAILURE) {
|
||||
const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope);
|
||||
|
@ -386,7 +387,7 @@ export class AzureActiveDirectoryService {
|
|||
}
|
||||
} else {
|
||||
await this.logout(token.sessionId);
|
||||
onDidChangeSessions.fire();
|
||||
onDidChangeSessions.fire({ added: [], removed: [token.sessionId], changed: [] });
|
||||
}
|
||||
}
|
||||
}, 1000 * (parseInt(token.expiresIn) - 30)));
|
||||
|
@ -548,9 +549,8 @@ export class AzureActiveDirectoryService {
|
|||
const token = this._tokens.find(token => token.sessionId === sessionId);
|
||||
if (token) {
|
||||
token.accessToken = undefined;
|
||||
onDidChangeSessions.fire({ added: [], removed: [], changed: [token.sessionId] });
|
||||
}
|
||||
|
||||
onDidChangeSessions.fire();
|
||||
}
|
||||
|
||||
const delayBeforeRetry = 5 * attempts * attempts;
|
||||
|
|
|
@ -25,13 +25,17 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
login: async (scopes: string[]) => {
|
||||
try {
|
||||
await loginService.login(scopes.sort().join(' '));
|
||||
const session = loginService.sessions[loginService.sessions.length - 1];
|
||||
onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] });
|
||||
return loginService.sessions[0]!;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
logout: async (id: string) => {
|
||||
return loginService.logout(id);
|
||||
await loginService.logout(id);
|
||||
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
|
||||
vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out."));
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -46,8 +50,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
}
|
||||
|
||||
if (sessions.length === 1) {
|
||||
await loginService.logout(loginService.sessions[0].id);
|
||||
onDidChangeSessions.fire();
|
||||
const id = loginService.sessions[0].id;
|
||||
await loginService.logout(id);
|
||||
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
|
||||
vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out."));
|
||||
return;
|
||||
}
|
||||
|
@ -61,7 +66,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
|
||||
if (selectedSession) {
|
||||
await loginService.logout(selectedSession.id);
|
||||
onDidChangeSessions.fire();
|
||||
onDidChangeSessions.fire({ added: [], removed: [selectedSession.id], changed: [] });
|
||||
vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out."));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1396,6 +1396,15 @@ export interface AuthenticationSession {
|
|||
accountName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface AuthenticationSessionsChangeEvent {
|
||||
added: string[];
|
||||
removed: string[];
|
||||
changed: string[];
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
|
@ -120,6 +120,7 @@ export class MenuId {
|
|||
static readonly TimelineItemContext = new MenuId('TimelineItemContext');
|
||||
static readonly TimelineTitle = new MenuId('TimelineTitle');
|
||||
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
|
||||
static readonly AccountsContext = new MenuId('AccountsContext');
|
||||
|
||||
readonly id: number;
|
||||
readonly _debugName: string;
|
||||
|
|
24
src/vs/vscode.proposed.d.ts
vendored
24
src/vs/vscode.proposed.d.ts
vendored
|
@ -40,6 +40,26 @@ declare module 'vscode' {
|
|||
readonly removed: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed.
|
||||
*/
|
||||
export interface AuthenticationSessionsChangeEvent {
|
||||
/**
|
||||
* The ids of the [AuthenticationSession](#AuthenticationSession)s that have been added.
|
||||
*/
|
||||
readonly added: string[];
|
||||
|
||||
/**
|
||||
* The ids of the [AuthenticationSession](#AuthenticationSession)s that have been removed.
|
||||
*/
|
||||
readonly removed: string[];
|
||||
|
||||
/**
|
||||
* The ids of the [AuthenticationSession](#AuthenticationSession)s that have been changed.
|
||||
*/
|
||||
readonly changed: string[];
|
||||
}
|
||||
|
||||
export interface AuthenticationProvider {
|
||||
/**
|
||||
* Used as an identifier for extensions trying to work with a particular
|
||||
|
@ -53,7 +73,7 @@ declare module 'vscode' {
|
|||
* An [event](#Event) which fires when the array of sessions has changed, or data
|
||||
* within a session has changed.
|
||||
*/
|
||||
readonly onDidChangeSessions: Event<void>;
|
||||
readonly onDidChangeSessions: Event<AuthenticationSessionsChangeEvent>;
|
||||
|
||||
/**
|
||||
* Returns an array of current sessions.
|
||||
|
@ -99,7 +119,7 @@ declare module 'vscode' {
|
|||
* within a session has changed for a provider. Fires with the ids of the providers
|
||||
* that have had session data change.
|
||||
*/
|
||||
export const onDidChangeSessions: Event<string[]>;
|
||||
export const onDidChangeSessions: Event<{ [providerId: string]: AuthenticationSessionsChangeEvent }>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable } 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,13 +12,141 @@ 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';
|
||||
|
||||
interface AuthDependent {
|
||||
providerId: string;
|
||||
label: string;
|
||||
scopes: string[];
|
||||
scopeDescriptions?: string;
|
||||
}
|
||||
|
||||
const BUILT_IN_AUTH_DEPENDENTS: AuthDependent[] = [
|
||||
{
|
||||
providerId: 'microsoft',
|
||||
label: 'Settings sync',
|
||||
scopes: ['https://management.core.windows.net/.default', 'offline_access'],
|
||||
scopeDescriptions: 'Read user email'
|
||||
}
|
||||
];
|
||||
|
||||
export class MainThreadAuthenticationProvider extends Disposable {
|
||||
private _sessionMenuItems = new Map<string, IDisposable[]>();
|
||||
private _sessionIds: string[] = [];
|
||||
|
||||
export class MainThreadAuthenticationProvider {
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostAuthenticationShape,
|
||||
public readonly id: string,
|
||||
public readonly displayName: string
|
||||
) { }
|
||||
public readonly displayName: string,
|
||||
public readonly dependents: AuthDependent[]
|
||||
) {
|
||||
super();
|
||||
|
||||
if (!dependents.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerCommandsAndContextMenuItems();
|
||||
}
|
||||
|
||||
private setPermissionsForAccount(quickInputService: IQuickInputService, doLogin?: boolean) {
|
||||
const quickPick = quickInputService.createQuickPick();
|
||||
quickPick.canSelectMany = true;
|
||||
const items = this.dependents.map(dependent => {
|
||||
return {
|
||||
label: dependent.label,
|
||||
description: dependent.scopeDescriptions,
|
||||
picked: true,
|
||||
scopes: dependent.scopes
|
||||
};
|
||||
});
|
||||
|
||||
quickPick.items = items;
|
||||
// TODO read from storage and filter is not doLogin
|
||||
quickPick.selectedItems = items;
|
||||
quickPick.title = nls.localize('signInTo', "Sign in to {0}", this.displayName);
|
||||
quickPick.placeholder = nls.localize('accountPermissions', "Choose what features and extensions to authorize to use this account");
|
||||
|
||||
quickPick.onDidAccept(() => {
|
||||
const scopes = quickPick.selectedItems.reduce((previous, current) => previous.concat((current as any).scopes), []);
|
||||
if (scopes.length && doLogin) {
|
||||
this.login(scopes);
|
||||
}
|
||||
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.onDidHide(() => {
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.show();
|
||||
}
|
||||
|
||||
private registerCommandsAndContextMenuItems(): void {
|
||||
this._register(CommandsRegistry.registerCommand({
|
||||
id: `signIn${this.id}`,
|
||||
handler: (accessor, args) => {
|
||||
this.setPermissionsForAccount(accessor.get(IQuickInputService), true);
|
||||
},
|
||||
}));
|
||||
|
||||
this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
|
||||
group: '2_providers',
|
||||
command: {
|
||||
id: `signIn${this.id}`,
|
||||
title: nls.localize('addAccount', "Sign in to {0}", this.displayName)
|
||||
},
|
||||
order: 3
|
||||
}));
|
||||
|
||||
this._proxy.$getSessions(this.id).then(sessions => {
|
||||
sessions.forEach(session => this.registerSession(session));
|
||||
});
|
||||
}
|
||||
|
||||
private registerSession(session: modes.AuthenticationSession) {
|
||||
this._sessionIds.push(session.id);
|
||||
const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
|
||||
group: '1_accounts',
|
||||
command: {
|
||||
id: `configureSessions${session.id}`,
|
||||
title: session.accountName
|
||||
},
|
||||
order: 3
|
||||
});
|
||||
|
||||
const manageCommand = CommandsRegistry.registerCommand({
|
||||
id: `configureSessions${session.id}`,
|
||||
handler: (accessor, args) => {
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
|
||||
const quickPick = quickInputService.createQuickPick();
|
||||
const items = [{ label: 'Sign Out' }];
|
||||
|
||||
quickPick.items = items;
|
||||
|
||||
quickPick.onDidAccept(e => {
|
||||
const selected = quickPick.selectedItems[0];
|
||||
if (selected.label === 'Sign Out') {
|
||||
this.logout(session.id);
|
||||
}
|
||||
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.onDidHide(_ => {
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.show();
|
||||
},
|
||||
});
|
||||
|
||||
this._sessionMenuItems.set(session.id, [menuItem, manageCommand]);
|
||||
}
|
||||
|
||||
async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> {
|
||||
return (await this._proxy.$getSessions(this.id)).map(session => {
|
||||
|
@ -30,6 +158,24 @@ export class MainThreadAuthenticationProvider {
|
|||
});
|
||||
}
|
||||
|
||||
async updateSessionItems(): Promise<void> {
|
||||
const currentSessions = await this._proxy.$getSessions(this.id);
|
||||
const removedSessionIds = this._sessionIds.filter(id => !currentSessions.some(session => session.id === id));
|
||||
const addedSessions = currentSessions.filter(session => !this._sessionIds.some(id => id === session.id));
|
||||
|
||||
removedSessionIds.forEach(id => {
|
||||
const disposeables = this._sessionMenuItems.get(id);
|
||||
if (disposeables) {
|
||||
disposeables.forEach(disposeable => disposeable.dispose());
|
||||
this._sessionMenuItems.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
addedSessions.forEach(session => this.registerSession(session));
|
||||
|
||||
this._sessionIds = currentSessions.map(session => session.id);
|
||||
}
|
||||
|
||||
login(scopes: string[]): Promise<modes.AuthenticationSession> {
|
||||
return this._proxy.$login(this.id, scopes).then(session => {
|
||||
return {
|
||||
|
@ -40,8 +186,14 @@ export class MainThreadAuthenticationProvider {
|
|||
});
|
||||
}
|
||||
|
||||
logout(accountId: string): Promise<void> {
|
||||
return this._proxy.$logout(this.id, accountId);
|
||||
logout(sessionId: string): Promise<void> {
|
||||
return this._proxy.$logout(this.id, sessionId);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this._sessionMenuItems.forEach(item => item.forEach(d => d.dispose()));
|
||||
this._sessionMenuItems.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,8 +211,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
|
||||
}
|
||||
|
||||
$registerAuthenticationProvider(id: string, displayName: string): void {
|
||||
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName);
|
||||
async $registerAuthenticationProvider(id: string, displayName: string): Promise<void> {
|
||||
const dependentBuiltIns = BUILT_IN_AUTH_DEPENDENTS.filter(dependency => dependency.providerId === id);
|
||||
|
||||
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, dependentBuiltIns);
|
||||
this.authenticationService.registerAuthenticationProvider(id, provider);
|
||||
}
|
||||
|
||||
|
@ -68,8 +222,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
this.authenticationService.unregisterAuthenticationProvider(id);
|
||||
}
|
||||
|
||||
$onDidChangeSessions(id: string): void {
|
||||
this.authenticationService.sessionsUpdate(id);
|
||||
$onDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
|
||||
this.authenticationService.sessionsUpdate(id, event);
|
||||
}
|
||||
|
||||
async $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
|
||||
|
|
|
@ -199,7 +199,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
login(providerId: string, scopes: string[]): Thenable<vscode.AuthenticationSession> {
|
||||
return extHostAuthentication.login(extension, providerId, scopes);
|
||||
},
|
||||
get onDidChangeSessions(): Event<string[]> {
|
||||
get onDidChangeSessions(): Event<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }> {
|
||||
return extHostAuthentication.onDidChangeSessions;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -156,7 +156,7 @@ export interface MainThreadCommentsShape extends IDisposable {
|
|||
export interface MainThreadAuthenticationShape extends IDisposable {
|
||||
$registerAuthenticationProvider(id: string, displayName: string): void;
|
||||
$unregisterAuthenticationProvider(id: string): void;
|
||||
$onDidChangeSessions(id: string): void;
|
||||
$onDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void;
|
||||
$getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
|
||||
$loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean>;
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
private _onDidChangeAuthenticationProviders = new Emitter<vscode.AuthenticationProvidersChangeEvent>();
|
||||
readonly onDidChangeAuthenticationProviders: Event<vscode.AuthenticationProvidersChangeEvent> = this._onDidChangeAuthenticationProviders.event;
|
||||
|
||||
private _onDidChangeSessions = new Emitter<string[]>();
|
||||
readonly onDidChangeSessions: Event<string[]> = this._onDidChangeSessions.event;
|
||||
private _onDidChangeSessions = new Emitter<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }>();
|
||||
readonly onDidChangeSessions: Event<{ [providerId: string]: vscode.AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event;
|
||||
|
||||
constructor(mainContext: IMainContext) {
|
||||
this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication);
|
||||
|
@ -85,9 +85,9 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
|
||||
this._authenticationProviders.set(provider.id, provider);
|
||||
|
||||
const listener = provider.onDidChangeSessions(_ => {
|
||||
this._proxy.$onDidChangeSessions(provider.id);
|
||||
this._onDidChangeSessions.fire([provider.id]);
|
||||
const listener = provider.onDidChangeSessions(e => {
|
||||
this._proxy.$onDidChangeSessions(provider.id, e);
|
||||
this._onDidChangeSessions.fire({ [provider.id]: e });
|
||||
});
|
||||
|
||||
this._proxy.$registerAuthenticationProvider(provider.id, provider.displayName);
|
||||
|
|
|
@ -134,6 +134,61 @@ export class ToggleViewletAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
export class AccountsActionViewItem extends ActivityActionViewItem {
|
||||
constructor(
|
||||
action: ActivityAction,
|
||||
colors: (theme: IColorTheme) => ICompositeBarColors,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextMenuService protected contextMenuService: IContextMenuService,
|
||||
@IMenuService protected menuService: IMenuService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super(action, { draggable: false, colors, icon: true }, themeService);
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
// Context menus are triggered on mouse down so that an item can be picked
|
||||
// and executed with releasing the mouse over it
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
this.showContextMenu();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
this.showContextMenu();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
this.showContextMenu();
|
||||
}));
|
||||
}
|
||||
|
||||
private showContextMenu(): void {
|
||||
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 };
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => location,
|
||||
getActions: () => accountsActions,
|
||||
onHide: () => {
|
||||
accountsMenu.dispose();
|
||||
dispose(actionsDisposable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class GlobalActivityActionViewItem extends ActivityActionViewItem {
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/acti
|
|||
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Part } from 'vs/workbench/browser/part';
|
||||
import { GlobalActivityActionViewItem, ViewletActivityAction, ToggleViewletAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewletActivityAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions';
|
||||
import { GlobalActivityActionViewItem, ViewletActivityAction, ToggleViewletAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewletActivityAction, AccountsActionViewItem } from 'vs/workbench/browser/parts/activitybar/activitybarActions';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity';
|
||||
import { IWorkbenchLayoutService, Parts, Position as SideBarPosition } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
|
@ -354,7 +354,17 @@ export class ActivitybarPart extends Part implements IActivityBarService {
|
|||
|
||||
private createGlobalActivityActionBar(container: HTMLElement): void {
|
||||
this.globalActivityActionBar = this._register(new ActionBar(container, {
|
||||
actionViewItemProvider: action => this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)),
|
||||
actionViewItemProvider: action => {
|
||||
if (action.id === 'workbench.actions.manage') {
|
||||
return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme));
|
||||
}
|
||||
|
||||
if (action.id === 'workbench.actions.accounts') {
|
||||
return this.instantiationService.createInstance(AccountsActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme));
|
||||
}
|
||||
|
||||
throw new Error(`No view item for action '${action.id}'`);
|
||||
},
|
||||
orientation: ActionsOrientation.VERTICAL,
|
||||
ariaLabel: nls.localize('manage', "Manage"),
|
||||
animated: false
|
||||
|
@ -366,6 +376,13 @@ export class ActivitybarPart extends Part implements IActivityBarService {
|
|||
cssClass: 'codicon-settings-gear'
|
||||
});
|
||||
|
||||
const profileAction = new ActivityAction({
|
||||
id: 'workbench.actions.accounts',
|
||||
name: nls.localize('accounts', "Accounts"),
|
||||
cssClass: 'codicon-account'
|
||||
});
|
||||
|
||||
this.globalActivityActionBar.push(profileAction);
|
||||
this.globalActivityActionBar.push(this.globalActivityAction);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import type { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
|||
import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import type { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import type { ITextModel } from 'vs/editor/common/model';
|
||||
import { AuthenticationSession } from 'vs/editor/common/modes';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
|
@ -63,6 +63,8 @@ const enum AuthStatus {
|
|||
const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey<string>('authTokenStatus', AuthStatus.Initializing);
|
||||
const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources', '');
|
||||
|
||||
const USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY = 'userDataSyncAccountPreference';
|
||||
|
||||
type ConfigureSyncQuickPickItem = { id: SyncResource, label: string, description?: string };
|
||||
|
||||
function getSyncAreaLabel(source: SyncResource): string {
|
||||
|
@ -195,18 +197,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedAccount = await this.quickInputService.pick(sessions.map(session => {
|
||||
return {
|
||||
id: session.id,
|
||||
label: session.accountName
|
||||
};
|
||||
}), { canPickMany: false });
|
||||
|
||||
if (selectedAccount) {
|
||||
const selected = sessions.filter(account => selectedAccount.id === account.id)[0];
|
||||
this.logAuthenticatedEvent(selected);
|
||||
await this.setActiveAccount(selected);
|
||||
const accountPreference = this.storageService.get(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, StorageScope.GLOBAL);
|
||||
if (accountPreference) {
|
||||
const matchingSession = sessions.find(session => session.id === accountPreference);
|
||||
if (matchingSession) {
|
||||
this.setActiveAccount(matchingSession);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.showSwitchAccountPicker(sessions);
|
||||
}
|
||||
|
||||
private logAuthenticatedEvent(session: AuthenticationSession): void {
|
||||
|
@ -246,15 +246,80 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
|||
this.updateBadge();
|
||||
}
|
||||
|
||||
private async onDidChangeSessions(providerId: string): Promise<void> {
|
||||
private async showSwitchAccountPicker(sessions: readonly AuthenticationSession[]): Promise<void> {
|
||||
return new Promise((resolve, _) => {
|
||||
const quickPick = this.quickInputService.createQuickPick<{ label: string, session: AuthenticationSession }>();
|
||||
quickPick.title = localize('chooseAccountTitle', "Sync: Choose Account");
|
||||
quickPick.placeholder = localize('chooseAccount', "Choose an account you would like to use for settings sync");
|
||||
quickPick.items = sessions.map(session => {
|
||||
return {
|
||||
label: session.accountName,
|
||||
session: session
|
||||
};
|
||||
});
|
||||
|
||||
quickPick.onDidHide(() => {
|
||||
quickPick.dispose();
|
||||
resolve();
|
||||
});
|
||||
|
||||
quickPick.onDidAccept(() => {
|
||||
const selected = quickPick.selectedItems[0];
|
||||
this.setActiveAccount(selected.session);
|
||||
this.storageService.store(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, selected.session.id, StorageScope.GLOBAL);
|
||||
quickPick.dispose();
|
||||
resolve();
|
||||
});
|
||||
|
||||
quickPick.show();
|
||||
});
|
||||
}
|
||||
|
||||
private async onDidChangeSessions(e: { providerId: string, event: AuthenticationSessionsChangeEvent }): Promise<void> {
|
||||
const { providerId, event } = e;
|
||||
if (providerId === this.userDataSyncStore!.authenticationProviderId) {
|
||||
if (this.activeAccount) {
|
||||
// Try to update existing account, case where access token has been refreshed
|
||||
const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
|
||||
const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0];
|
||||
this.setActiveAccount(matchingAccount);
|
||||
if (event.removed.length) {
|
||||
const activeWasRemoved = !!event.removed.find(removed => removed === this.activeAccount!.id);
|
||||
|
||||
// If the current account was removed, check if another account can be used, otherwise offer to turn off sync
|
||||
if (activeWasRemoved) {
|
||||
const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
|
||||
if (accounts.length) {
|
||||
// Show switch dialog here
|
||||
await this.showSwitchAccountPicker(accounts);
|
||||
} else {
|
||||
await this.turnOff();
|
||||
this.setActiveAccount(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (event.added.length) {
|
||||
// Offer to switch accounts
|
||||
const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
|
||||
await this.showSwitchAccountPicker(accounts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.changed.length) {
|
||||
const activeWasChanged = !!event.changed.find(changed => changed === this.activeAccount!.id);
|
||||
if (activeWasChanged) {
|
||||
// Try to update existing account, case where access token has been refreshed
|
||||
const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
|
||||
const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0];
|
||||
this.setActiveAccount(matchingAccount);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.initializeActiveAccount();
|
||||
await this.initializeActiveAccount();
|
||||
|
||||
// If logged in for the first time from accounts menu, prompt if sync should be turned on
|
||||
if (this.activeAccount) {
|
||||
this.turnOn(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -520,7 +585,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
|||
}
|
||||
}
|
||||
|
||||
private async turnOn(): Promise<void> {
|
||||
private async turnOn(skipAccountPick?: boolean): Promise<void> {
|
||||
if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) {
|
||||
const result = await this.dialogService.show(
|
||||
Severity.Info,
|
||||
|
@ -562,7 +627,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
|||
disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => {
|
||||
if (quickPick.selectedItems.length) {
|
||||
this.updateConfiguration(items, quickPick.selectedItems);
|
||||
this.doTurnOn().then(c, e);
|
||||
this.doTurnOn(skipAccountPick).then(c, e);
|
||||
quickPick.hide();
|
||||
}
|
||||
}));
|
||||
|
@ -571,8 +636,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
|||
});
|
||||
}
|
||||
|
||||
private async doTurnOn(): Promise<void> {
|
||||
if (this.authenticationState.get() === AuthStatus.SignedIn) {
|
||||
private async doTurnOn(skipAccountPick?: boolean): Promise<void> {
|
||||
if (this.authenticationState.get() === AuthStatus.SignedIn && !skipAccountPick) {
|
||||
await new Promise((c, e) => {
|
||||
const disposables: DisposableStore = new DisposableStore();
|
||||
const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId);
|
||||
|
@ -706,7 +771,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
|||
private async turnOff(): Promise<void> {
|
||||
const result = await this.dialogService.confirm({
|
||||
type: 'info',
|
||||
message: localize('turn off sync confirmation', "Turn off Sync"),
|
||||
message: localize('turn off sync confirmation', "Do you want to turn off sync?"),
|
||||
detail: localize('turn off sync detail', "Your settings, keybindings, extensions and UI State will no longer be synced."),
|
||||
primaryButton: localize('turn off', "Turn Off"),
|
||||
checkbox: {
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { AuthenticationSession } from 'vs/editor/common/modes';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { MainThreadAuthenticationProvider } from 'vs/workbench/api/browser/mainThreadAuthentication';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
|
||||
export const IAuthenticationService = createDecorator<IAuthenticationService>('IAuthenticationService');
|
||||
|
||||
|
@ -17,12 +19,12 @@ export interface IAuthenticationService {
|
|||
|
||||
registerAuthenticationProvider(id: string, provider: MainThreadAuthenticationProvider): void;
|
||||
unregisterAuthenticationProvider(id: string): void;
|
||||
sessionsUpdate(providerId: string): void;
|
||||
sessionsUpdate(providerId: string, event: AuthenticationSessionsChangeEvent): void;
|
||||
|
||||
readonly onDidRegisterAuthenticationProvider: Event<string>;
|
||||
readonly onDidUnregisterAuthenticationProvider: Event<string>;
|
||||
|
||||
readonly onDidChangeSessions: Event<string>;
|
||||
readonly onDidChangeSessions: Event<{ providerId: string, event: AuthenticationSessionsChangeEvent }>;
|
||||
getSessions(providerId: string): Promise<ReadonlyArray<AuthenticationSession> | undefined>;
|
||||
getDisplayName(providerId: string): string;
|
||||
login(providerId: string, scopes: string[]): Promise<AuthenticationSession>;
|
||||
|
@ -31,6 +33,7 @@ export interface IAuthenticationService {
|
|||
|
||||
export class AuthenticationService extends Disposable implements IAuthenticationService {
|
||||
_serviceBrand: undefined;
|
||||
private _placeholderMenuItem: IDisposable | undefined;
|
||||
|
||||
private _authenticationProviders: Map<string, MainThreadAuthenticationProvider> = new Map<string, MainThreadAuthenticationProvider>();
|
||||
|
||||
|
@ -40,25 +43,53 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
|||
private _onDidUnregisterAuthenticationProvider: Emitter<string> = this._register(new Emitter<string>());
|
||||
readonly onDidUnregisterAuthenticationProvider: Event<string> = this._onDidUnregisterAuthenticationProvider.event;
|
||||
|
||||
private _onDidChangeSessions: Emitter<string> = this._register(new Emitter<string>());
|
||||
readonly onDidChangeSessions: Event<string> = this._onDidChangeSessions.event;
|
||||
private _onDidChangeSessions: Emitter<{ providerId: string, event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string, event: AuthenticationSessionsChangeEvent }>());
|
||||
readonly onDidChangeSessions: Event<{ providerId: string, event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
|
||||
command: {
|
||||
id: 'noAuthenticationProviders',
|
||||
title: nls.localize('noAuthenticationProviders', "No authentication providers registered")
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registerAuthenticationProvider(id: string, authenticationProvider: MainThreadAuthenticationProvider): void {
|
||||
this._authenticationProviders.set(id, authenticationProvider);
|
||||
this._onDidRegisterAuthenticationProvider.fire(id);
|
||||
|
||||
if (authenticationProvider.dependents.length && this._placeholderMenuItem) {
|
||||
this._placeholderMenuItem.dispose();
|
||||
this._placeholderMenuItem = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
unregisterAuthenticationProvider(id: string): void {
|
||||
this._authenticationProviders.delete(id);
|
||||
this._onDidUnregisterAuthenticationProvider.fire(id);
|
||||
const provider = this._authenticationProviders.get(id);
|
||||
if (provider) {
|
||||
provider.dispose();
|
||||
this._authenticationProviders.delete(id);
|
||||
this._onDidUnregisterAuthenticationProvider.fire(id);
|
||||
}
|
||||
|
||||
if (!this._authenticationProviders.size) {
|
||||
this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
|
||||
command: {
|
||||
id: 'noAuthenticationProviders',
|
||||
title: nls.localize('noAuthenticationProviders', "No authentication providers registered")
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sessionsUpdate(id: string): void {
|
||||
this._onDidChangeSessions.fire(id);
|
||||
sessionsUpdate(id: string, event: AuthenticationSessionsChangeEvent): void {
|
||||
this._onDidChangeSessions.fire({ providerId: id, event: event });
|
||||
const provider = this._authenticationProviders.get(id);
|
||||
if (provider) {
|
||||
provider.updateSessionItems();
|
||||
}
|
||||
}
|
||||
|
||||
getDisplayName(id: string): string {
|
||||
|
|
Loading…
Reference in a new issue