More Layering (#9139)

* move handling generated files to the serilization classes

* remove unneeded methods

* add more folders to strictire compile, add more strict compile options

* update ci

* wip

* add more layering and fix issues

* add more strictness

* remove unnecessary assertion

* add missing checks

* fix indentation

* wip

* remove jsdoc

* fix layering

* fix compile

* fix compile errors

* wip

* wip

* finish layering

* fix css

* more layering

* rip

* reworking results serializer

* move some files around

* move capabilities to platform wip

* implement capabilities register provider

* fix capabilities service

* fix usage of the regist4ry

* add contribution

* wip

* wip

* wip

* remove no longer good parts

* fix strict-nulls

* fix issues with startup

* another try

* fix startup

* fix imports

* fix tests

* fix tests

* fix more tests

* fix tests

* fix more tests

* fix broken test

* fix tabbing

* fix naming

* wip

* finished layering

* fix imports

* fix valid layers

* fix layers
This commit is contained in:
Anthony Dresser
2020-02-15 01:54:23 -06:00
committed by GitHub
parent 873c6a39fe
commit 506c6a5e5f
338 changed files with 815 additions and 724 deletions

View File

@@ -0,0 +1,367 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/accountDialog';
import 'vs/css!./media/accountActions';
import * as DOM from 'vs/base/browser/dom';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { Event, Emitter } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { IAction } from 'vs/base/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { values } from 'vs/base/common/map';
import * as azdata from 'azdata';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
import { AccountViewModel } from 'sql/platform/accounts/common/accountViewModel';
import { AddAccountAction } from 'sql/platform/accounts/common/accountActions';
import { AccountListRenderer, AccountListDelegate } from 'sql/workbench/services/accountManagement/browser/accountListRenderer';
import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/platform/accounts/common/eventTypes';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { attachModalDialogStyler, attachPanelStyler } from 'sql/workbench/common/styler';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
class AccountPanel extends ViewPane {
public index: number;
private accountList: List<azdata.Account>;
constructor(
private options: IViewPaneOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IOpenerService openerService: IOpenerService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService);
}
protected renderBody(container: HTMLElement): void {
this.accountList = new List<azdata.Account>('AccountList', container, new AccountListDelegate(AccountDialog.ACCOUNTLIST_HEIGHT), [this.instantiationService.createInstance(AccountListRenderer)]);
this._register(attachListStyler(this.accountList, this.themeService));
}
protected layoutBody(size: number): void {
if (this.accountList) {
this.accountList.layout(size);
}
}
public get length(): number {
return this.accountList.length;
}
public focus() {
this.accountList.domFocus();
}
public updateAccounts(accounts: azdata.Account[]) {
this.accountList.splice(0, this.accountList.length, accounts);
}
public setSelection(indexes: number[]) {
this.accountList.setSelection(indexes);
}
public getActions(): IAction[] {
return [this.instantiationService.createInstance(
AddAccountAction,
this.options.id
)];
}
}
export interface IProviderViewUiComponent {
view: AccountPanel;
addAccountAction: AddAccountAction;
}
export class AccountDialog extends Modal {
public static ACCOUNTLIST_HEIGHT = 77;
public viewModel: AccountViewModel;
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _providerViewsMap = new Map<string, IProviderViewUiComponent>();
private _closeButton: Button;
private _addAccountButton: Button;
private _splitView: SplitView;
private _container: HTMLElement;
private _splitViewContainer: HTMLElement;
private _noaccountViewContainer: HTMLElement;
// EVENTING ////////////////////////////////////////////////////////////
private _onAddAccountErrorEmitter: Emitter<string>;
public get onAddAccountErrorEvent(): Event<string> { return this._onAddAccountErrorEmitter.event; }
private _onCloseEmitter: Emitter<void>;
public get onCloseEvent(): Event<void> { return this._onCloseEmitter.event; }
constructor(
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IThemeService themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IKeybindingService private _keybindingService: IKeybindingService,
@IConfigurationService private _configurationService: IConfigurationService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@IViewDescriptorService private viewDescriptorService: IViewDescriptorService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
@IOpenerService protected readonly openerService: IOpenerService
) {
super(
localize('linkedAccounts', "Linked accounts"),
TelemetryKeys.Accounts,
telemetryService,
layoutService,
clipboardService,
themeService,
logService,
textResourcePropertiesService,
contextKeyService,
{ hasSpinner: true }
);
// Setup the event emitters
this._onAddAccountErrorEmitter = new Emitter<string>();
this._onCloseEmitter = new Emitter<void>();
// Create the view model and wire up the events
this.viewModel = this._instantiationService.createInstance(AccountViewModel);
this.viewModel.addProviderEvent(arg => { this.addProvider(arg); });
this.viewModel.removeProviderEvent(arg => { this.removeProvider(arg); });
this.viewModel.updateAccountListEvent(arg => { this.updateProviderAccounts(arg); });
// Load the initial contents of the view model
this.viewModel.initialize()
.then(addedProviders => {
for (const addedProvider of addedProviders) {
this.addProvider(addedProvider);
}
});
}
// MODAL OVERRIDE METHODS //////////////////////////////////////////////
protected layout(height?: number): void {
this._splitView.layout(DOM.getContentHeight(this._container));
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
this._closeButton = this.addFooterButton(localize('accountDialog.close', "Close"), () => this.close());
this.registerListeners();
}
protected renderBody(container: HTMLElement) {
this._container = container;
this._splitViewContainer = DOM.$('div.account-view.monaco-pane-view');
DOM.append(container, this._splitViewContainer);
this._splitView = new SplitView(this._splitViewContainer);
this._noaccountViewContainer = DOM.$('div.no-account-view');
const noAccountTitle = DOM.append(this._noaccountViewContainer, DOM.$('.no-account-view-label'));
const noAccountLabel = localize('accountDialog.noAccountLabel', "There is no linked account. Please add an account.");
noAccountTitle.innerText = noAccountLabel;
// Show the add account button for the first provider
// Todo: If we have more than 1 provider, need to show all add account buttons for all providers
const buttonSection = DOM.append(this._noaccountViewContainer, DOM.$('div.button-section'));
this._addAccountButton = new Button(buttonSection);
this._addAccountButton.label = localize('accountDialog.addConnection', "Add an account");
this._register(this._addAccountButton.onDidClick(() => {
(<IProviderViewUiComponent>values(this._providerViewsMap)[0]).addAccountAction.run();
}));
DOM.append(container, this._noaccountViewContainer);
}
private registerListeners(): void {
// Theme styler
this._register(attachButtonStyler(this._closeButton, this._themeService));
this._register(attachButtonStyler(this._addAccountButton, this._themeService));
}
/* Overwrite escape key behavior */
protected onClose() {
this.close();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.close();
}
public close() {
this._onCloseEmitter.fire();
this.hide();
}
public open() {
this.show();
if (!this.isEmptyLinkedAccount()) {
this.showSplitView();
} else {
this.showNoAccountContainer();
}
}
private showNoAccountContainer() {
this._splitViewContainer.hidden = true;
this._noaccountViewContainer.hidden = false;
this._addAccountButton.focus();
}
private showSplitView() {
this._splitViewContainer.hidden = false;
this._noaccountViewContainer.hidden = true;
if (values(this._providerViewsMap).length > 0) {
const firstView = values(this._providerViewsMap)[0];
if (firstView instanceof AccountPanel) {
firstView.setSelection([0]);
firstView.focus();
}
}
}
private isEmptyLinkedAccount(): boolean {
for (const provider of values(this._providerViewsMap)) {
const listView = provider.view;
if (listView && listView.length > 0) {
return false;
}
}
return true;
}
public dispose(): void {
super.dispose();
for (const provider of values(this._providerViewsMap)) {
if (provider.addAccountAction) {
provider.addAccountAction.dispose();
}
if (provider.view) {
provider.view.dispose();
}
}
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private addProvider(newProvider: AccountProviderAddedEventParams) {
// Skip adding the provider if it already exists
if (this._providerViewsMap.get(newProvider.addedProvider.id)) {
return;
}
// Account provider doesn't exist, so add it
// Create a scoped add account action
let addAccountAction = this._instantiationService.createInstance(
AddAccountAction,
newProvider.addedProvider.id
);
addAccountAction.addAccountCompleteEvent(() => this.spinner = false);
addAccountAction.addAccountErrorEvent(msg => this._onAddAccountErrorEmitter.fire(msg));
addAccountAction.addAccountStartEvent(() => this.spinner = true);
let providerView = new AccountPanel(
{
id: newProvider.addedProvider.id,
title: newProvider.addedProvider.displayName,
ariaHeaderLabel: newProvider.addedProvider.displayName
},
this._keybindingService,
this._contextMenuService,
this._configurationService,
this._themeService,
this.contextKeyService,
this._instantiationService,
this.viewDescriptorService,
this.openerService
);
attachPanelStyler(providerView, this._themeService);
const insertIndex = this._splitView.length;
// Append the list view to the split view
this._splitView.addView(providerView, Sizing.Distribute, insertIndex);
providerView.render();
providerView.index = insertIndex;
this._splitView.layout(DOM.getContentHeight(this._container));
// Set the initial items of the list
providerView.updateAccounts(newProvider.initialAccounts);
if (newProvider.initialAccounts.length > 0 && this._splitViewContainer.hidden) {
this.showSplitView();
}
this.layout();
// Store the view for the provider and action
this._providerViewsMap.set(newProvider.addedProvider.id, { view: providerView, addAccountAction: addAccountAction });
}
private removeProvider(removedProvider: azdata.AccountProviderMetadata) {
// Skip removing the provider if it doesn't exist
const providerView = this._providerViewsMap.get(removedProvider.id);
if (!providerView || !providerView.view) {
return;
}
// Remove the list view from the split view
this._splitView.removeView(providerView.view.index);
this._splitView.layout(DOM.getContentHeight(this._container));
// Remove the list view from our internal map
this._providerViewsMap.delete(removedProvider.id);
this.layout();
}
private updateProviderAccounts(args: UpdateAccountListEventParams) {
const providerMapping = this._providerViewsMap.get(args.providerId);
if (!providerMapping || !providerMapping.view) {
return;
}
providerMapping.view.updateAccounts(args.accountList);
if (args.accountList.length > 0 && this._splitViewContainer.hidden) {
this.showSplitView();
}
if (this.isEmptyLinkedAccount() && this._noaccountViewContainer.hidden) {
this.showNoAccountContainer();
}
this.layout();
}
}

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Severity from 'vs/base/common/severity';
import { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
export class AccountDialogController {
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _addAccountErrorTitle = localize('accountDialog.addAccountErrorTitle', "Error adding account");
private _accountDialog: AccountDialog;
public get accountDialog(): AccountDialog { return this._accountDialog; }
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
) { }
/**
* Open account dialog
*/
public openAccountDialog(): void {
// Create a new dialog if one doesn't exist
if (!this._accountDialog) {
this._accountDialog = this._instantiationService.createInstance(AccountDialog);
this._accountDialog.onAddAccountErrorEvent(msg => this.handleOnAddAccountError(msg));
this._accountDialog.onCloseEvent(() => this.handleOnClose());
this._accountDialog.render();
}
// Open the dialog
this._accountDialog.open();
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private handleOnClose(): void { }
private handleOnAddAccountError(msg: string): void {
this._errorMessageService.showDialog(Severity.Error, this._addAccountErrorTitle, msg);
}
}

View File

@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/accountListRenderer';
import 'vs/css!./media/accountActions';
import * as DOM from 'vs/base/browser/dom';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ActionBar, IActionOptions } from 'vs/base/browser/ui/actionbar/actionbar';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { RemoveAccountAction, RefreshAccountAction } from 'sql/platform/accounts/common/accountActions';
import * as azdata from 'azdata';
export class AccountListDelegate implements IListVirtualDelegate<azdata.Account> {
constructor(
private _height: number
) {
}
public getHeight(element: azdata.Account): number {
return this._height;
}
public getTemplateId(element: azdata.Account): string {
return 'accountListRenderer';
}
}
export interface AccountPickerListTemplate {
root: HTMLElement;
icon: HTMLElement;
badgeContent: HTMLElement;
contextualDisplayName: HTMLElement;
label: HTMLElement;
displayName: HTMLElement;
}
export interface AccountListTemplate extends AccountPickerListTemplate {
content: HTMLElement;
actions: ActionBar;
}
export class AccountPickerListRenderer implements IListRenderer<azdata.Account, AccountPickerListTemplate> {
public static TEMPLATE_ID = 'accountListRenderer';
public get templateId(): string {
return AccountPickerListRenderer.TEMPLATE_ID;
}
public renderTemplate(container: HTMLElement): AccountPickerListTemplate {
const tableTemplate: AccountPickerListTemplate = Object.create(null);
const badge = DOM.$('div.badge');
tableTemplate.root = DOM.append(container, DOM.$('div.list-row.account-picker-list'));
tableTemplate.icon = DOM.append(tableTemplate.root, DOM.$('div.codicon'));
DOM.append(tableTemplate.icon, badge);
tableTemplate.badgeContent = DOM.append(badge, DOM.$('div.badge-content'));
tableTemplate.label = DOM.append(tableTemplate.root, DOM.$('div.label'));
tableTemplate.contextualDisplayName = DOM.append(tableTemplate.label, DOM.$('div.contextual-display-name'));
tableTemplate.displayName = DOM.append(tableTemplate.label, DOM.$('div.display-name'));
return tableTemplate;
}
public renderElement(account: azdata.Account, index: number, templateData: AccountPickerListTemplate): void {
// Set the account icon
templateData.icon.classList.add('account-logo', account.displayInfo.accountType);
templateData.contextualDisplayName.innerText = account.displayInfo.contextualDisplayName;
// show the account display name text, something like "User Name (user@email.com)"
if (account.displayInfo.userId && account.displayInfo.displayName) {
templateData.displayName.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.userId + ')';
} else {
templateData.displayName.innerText = account.displayInfo.displayName;
}
if (account.isStale) {
templateData.badgeContent.className = 'badge-content codicon warning-badge';
} else {
templateData.badgeContent.className = 'badge-content';
}
}
public disposeTemplate(template: AccountPickerListTemplate): void {
// noop
}
public disposeElement(element: azdata.Account, index: number, templateData: AccountPickerListTemplate): void {
// noop
}
}
export class AccountListRenderer extends AccountPickerListRenderer {
constructor(
@IInstantiationService private _instantiationService: IInstantiationService
) {
super();
}
public get templateId(): string {
return AccountListRenderer.TEMPLATE_ID;
}
public renderTemplate(container: HTMLElement): AccountListTemplate {
const tableTemplate = super.renderTemplate(container) as AccountListTemplate;
tableTemplate.content = DOM.append(tableTemplate.label, DOM.$('div.content'));
tableTemplate.actions = new ActionBar(tableTemplate.root, { animated: false });
return tableTemplate;
}
public renderElement(account: azdata.Account, index: number, templateData: AccountListTemplate): void {
super.renderElement(account, index, templateData);
if (account.isStale) {
templateData.content.innerText = localize('refreshCredentials', "You need to refresh the credentials for this account.");
} else {
templateData.content.innerText = '';
}
templateData.actions.clear();
const actionOptions: IActionOptions = { icon: true, label: false };
if (account.isStale) {
const refreshAction = this._instantiationService.createInstance(RefreshAccountAction);
refreshAction.account = account;
templateData.actions.push(refreshAction, actionOptions);
} else {
// Todo: Will show filter action when API/GUI for filtering is implemented (#3022, #3024)
// templateData.actions.push(new ApplyFilterAction(ApplyFilterAction.ID, ApplyFilterAction.LABEL), actionOptions);
}
const removeAction = this._instantiationService.createInstance(RemoveAccountAction, account);
templateData.actions.push(removeAction, actionOptions);
}
}

View File

@@ -12,8 +12,8 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
import { Memento } from 'vs/workbench/common/memento';
import AccountStore from 'sql/platform/accounts/common/accountStore';
import { AccountDialogController } from 'sql/workbench/contrib/accounts/browser/accountDialogController';
import { AutoOAuthDialogController } from 'sql/workbench/contrib/accounts/browser/autoOAuthDialogController';
import { AccountDialogController } from 'sql/workbench/services/accountManagement/browser/accountDialogController';
import { AutoOAuthDialogController } from 'sql/workbench/services/accountManagement/browser/autoOAuthDialogController';
import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/platform/accounts/common/eventTypes';
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
import { Deferred } from 'sql/base/common/promise';

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import * as azdata from 'azdata';
export const IAccountPickerService = createDecorator<IAccountPickerService>('AccountPickerService');
export interface IAccountPickerService {
_serviceBrand: undefined;
renderAccountPicker(container: HTMLElement): void;
addAccountCompleteEvent: Event<void>;
addAccountErrorEvent: Event<string>;
addAccountStartEvent: Event<void>;
onAccountSelectionChangeEvent: Event<azdata.Account | undefined>;
selectedAccount: azdata.Account | undefined;
}

View File

@@ -0,0 +1,237 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/accountPicker';
import * as DOM from 'vs/base/browser/dom';
import { Event, Emitter } from 'vs/base/common/event';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IDropdownOptions } from 'vs/base/browser/ui/dropdown/dropdown';
import { IListEvent } from 'vs/base/browser/ui/list/list';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { buttonBackground } from 'vs/platform/theme/common/colorRegistry';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import * as azdata from 'azdata';
import { DropdownList } from 'sql/base/browser/ui/dropdownList/dropdownList';
import { attachDropdownStyler } from 'sql/platform/theme/common/styler';
import { AddAccountAction, RefreshAccountAction } from 'sql/platform/accounts/common/accountActions';
import { AccountPickerListRenderer, AccountListDelegate } from 'sql/workbench/services/accountManagement/browser/accountListRenderer';
import { AccountPickerViewModel } from 'sql/platform/accounts/common/accountPickerViewModel';
import { firstIndex } from 'vs/base/common/arrays';
export class AccountPicker extends Disposable {
public static ACCOUNTPICKERLIST_HEIGHT = 47;
public viewModel: AccountPickerViewModel;
private _accountList: List<azdata.Account>;
private _rootElement: HTMLElement;
private _refreshContainer: HTMLElement;
private _listContainer: HTMLElement;
private _dropdown: DropdownList;
private _refreshAccountAction: RefreshAccountAction;
// EVENTING ////////////////////////////////////////////////////////////
private _addAccountCompleteEmitter: Emitter<void>;
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
private _addAccountErrorEmitter: Emitter<string>;
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
private _addAccountStartEmitter: Emitter<void>;
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
private _onAccountSelectionChangeEvent: Emitter<azdata.Account | undefined>;
public get onAccountSelectionChangeEvent(): Event<azdata.Account | undefined> { return this._onAccountSelectionChangeEvent.event; }
constructor(
private _providerId: string,
@IThemeService private _themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IContextViewService private _contextViewService: IContextViewService
) {
super();
// Create event emitters
this._addAccountCompleteEmitter = new Emitter<void>();
this._addAccountErrorEmitter = new Emitter<string>();
this._addAccountStartEmitter = new Emitter<void>();
this._onAccountSelectionChangeEvent = new Emitter<azdata.Account>();
// Create the view model, wire up the events, and initialize with baseline data
this.viewModel = this._instantiationService.createInstance(AccountPickerViewModel, this._providerId);
this.viewModel.updateAccountListEvent(arg => {
if (arg.providerId === this._providerId) {
this.updateAccountList(arg.accountList);
}
});
}
// PUBLIC METHODS //////////////////////////////////////////////////////
/**
* Render account picker
*/
public render(container: HTMLElement): void {
DOM.append(container, this._rootElement);
}
// PUBLIC METHODS //////////////////////////////////////////////////////
/**
* Create account picker component
*/
public createAccountPickerComponent() {
// Create an account list
const delegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT);
const accountRenderer = new AccountPickerListRenderer();
this._listContainer = DOM.$('div.account-list-container');
this._accountList = new List<azdata.Account>('AccountPicker', this._listContainer, delegate, [accountRenderer]);
this._register(attachListStyler(this._accountList, this._themeService));
this._rootElement = DOM.$('div.account-picker-container');
// Create a dropdown for account picker
const option: IDropdownOptions = {
contextViewProvider: this._contextViewService,
labelRenderer: (container) => this.renderLabel(container)
};
// Create the add account action
const addAccountAction = this._instantiationService.createInstance(AddAccountAction, this._providerId);
addAccountAction.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());
addAccountAction.addAccountErrorEvent((msg) => this._addAccountErrorEmitter.fire(msg));
addAccountAction.addAccountStartEvent(() => this._addAccountStartEmitter.fire());
this._dropdown = this._register(new DropdownList(this._rootElement, option, this._listContainer, this._accountList, addAccountAction));
this._register(attachDropdownStyler(this._dropdown, this._themeService));
this._register(this._accountList.onSelectionChange((e: IListEvent<azdata.Account>) => {
if (e.elements.length === 1) {
this._dropdown.renderLabel();
this.onAccountSelectionChange(e.elements[0]);
}
}));
// Create refresh account action
this._refreshContainer = DOM.append(this._rootElement, DOM.$('div.refresh-container'));
DOM.append(this._refreshContainer, DOM.$('div.sql codicon warning'));
const actionBar = new ActionBar(this._refreshContainer, { animated: false });
this._refreshAccountAction = this._instantiationService.createInstance(RefreshAccountAction);
actionBar.push(this._refreshAccountAction, { icon: false, label: true });
if (this._accountList.length > 0) {
this._accountList.setSelection([0]);
this.onAccountSelectionChange(this._accountList.getSelectedElements()[0]);
} else {
DOM.hide(this._refreshContainer);
}
this._register(this._themeService.onThemeChange(e => this.updateTheme(e)));
this.updateTheme(this._themeService.getTheme());
// Load the initial contents of the view model
this.viewModel.initialize()
.then((accounts: azdata.Account[]) => {
this.updateAccountList(accounts);
});
}
public dispose() {
super.dispose();
if (this._accountList) {
this._accountList.dispose();
}
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private onAccountSelectionChange(account: azdata.Account | undefined) {
this.viewModel.selectedAccount = account;
if (account && account.isStale) {
this._refreshAccountAction.account = account;
DOM.show(this._refreshContainer);
} else {
DOM.hide(this._refreshContainer);
}
this._onAccountSelectionChangeEvent.fire(account);
}
private renderLabel(container: HTMLElement): IDisposable | null {
if (container.hasChildNodes()) {
for (let i = 0; i < container.childNodes.length; i++) {
container.removeChild(container.childNodes.item(i));
}
}
const selectedAccounts = this._accountList.getSelectedElements();
const account = selectedAccounts ? selectedAccounts[0] : undefined;
if (account) {
const badge = DOM.$('div.badge');
const row = DOM.append(container, DOM.$('div.selected-account-container'));
const icon = DOM.append(row, DOM.$('div.codicon'));
DOM.append(icon, badge);
const badgeContent = DOM.append(badge, DOM.$('div.badge-content'));
const label = DOM.append(row, DOM.$('div.label'));
// Set the account icon
icon.classList.add('icon', account.displayInfo.accountType);
// TODO: Pick between the light and dark logo
label.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.contextualDisplayName + ')';
if (account.isStale) {
badgeContent.className = 'badge-content codicon warning-badge';
} else {
badgeContent.className = 'badge-content';
}
} else {
const row = DOM.append(container, DOM.$('div.no-account-container'));
row.innerText = AddAccountAction.LABEL + '...';
}
return null;
}
private updateAccountList(accounts: azdata.Account[]): void {
// keep the selection to the current one
const selectedElements = this._accountList.getSelectedElements();
// find selected index
let selectedIndex: number | undefined;
if (selectedElements.length > 0 && accounts.length > 0) {
selectedIndex = firstIndex(accounts, (account) => {
return (account.key.accountId === selectedElements[0].key.accountId);
});
}
// Replace the existing list with the new one
this._accountList.splice(0, this._accountList.length, accounts);
if (this._accountList.length > 0) {
if (selectedIndex && selectedIndex !== -1) {
this._accountList.setSelection([selectedIndex]);
} else {
this._accountList.setSelection([0]);
}
} else {
// if the account is empty, re-render dropdown label
this.onAccountSelectionChange(undefined);
this._dropdown.renderLabel();
}
this._accountList.layout(this._accountList.contentHeight);
}
/**
* Update theming that is specific to account picker
*/
private updateTheme(theme: ITheme): void {
const linkColor = theme.getColor(buttonBackground);
const link = linkColor ? linkColor.toString() : null;
this._refreshContainer.style.color = link;
if (this._refreshContainer) {
this._refreshContainer.style.color = link;
}
}
}

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import * as azdata from 'azdata';
import { IAccountPickerService } from 'sql/workbench/services/accountManagement/browser/accountPicker';
import { AccountPicker } from 'sql/workbench/services/accountManagement/browser/accountPickerImpl';
export class AccountPickerService implements IAccountPickerService {
_serviceBrand: undefined;
private _accountPicker: AccountPicker;
// EVENTING ////////////////////////////////////////////////////////////
private _addAccountCompleteEmitter: Emitter<void>;
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
private _addAccountErrorEmitter: Emitter<string>;
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
private _addAccountStartEmitter: Emitter<void>;
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
private _onAccountSelectionChangeEvent: Emitter<azdata.Account | undefined>;
public get onAccountSelectionChangeEvent(): Event<azdata.Account | undefined> { return this._onAccountSelectionChangeEvent.event; }
constructor(
@IInstantiationService private _instantiationService: IInstantiationService
) {
// Create event emitters
this._addAccountCompleteEmitter = new Emitter<void>();
this._addAccountErrorEmitter = new Emitter<string>();
this._addAccountStartEmitter = new Emitter<void>();
this._onAccountSelectionChangeEvent = new Emitter<azdata.Account>();
}
/**
* Get selected account
*/
public get selectedAccount(): azdata.Account | undefined {
return this._accountPicker.viewModel.selectedAccount;
}
/**
* Render account picker
*/
public renderAccountPicker(container: HTMLElement): void {
if (!this._accountPicker) {
// TODO: expand support to multiple providers
const providerId: string = 'azurePublicCloud';
this._accountPicker = this._instantiationService.createInstance(AccountPicker, providerId);
this._accountPicker.createAccountPickerComponent();
}
this._accountPicker.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());
this._accountPicker.addAccountErrorEvent((msg) => this._addAccountErrorEmitter.fire(msg));
this._accountPicker.addAccountStartEvent(() => this._addAccountStartEmitter.fire());
this._accountPicker.onAccountSelectionChangeEvent((account) => this._onAccountSelectionChangeEvent.fire(account));
this._accountPicker.render(container);
}
}

View File

@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/autoOAuthDialog';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { Event, Emitter } from 'vs/base/common/event';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { localize } from 'vs/nls';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { $, append } from 'vs/base/browser/dom';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
export class AutoOAuthDialog extends Modal {
private _copyAndOpenButton: Button;
private _closeButton: Button;
private _userCodeInputBox: InputBox;
private _websiteInputBox: InputBox;
private _descriptionElement: HTMLElement;
// EVENTING ////////////////////////////////////////////////////////////
private _onHandleAddAccount = new Emitter<void>();
public get onHandleAddAccount(): Event<void> { return this._onHandleAddAccount.event; }
private _onCancel = new Emitter<void>();
public get onCancel(): Event<void> { return this._onCancel.event; }
private _onCloseEvent = new Emitter<void>();
public get onCloseEvent(): Event<void> { return this._onCloseEvent.event; }
constructor(
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IThemeService themeService: IThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
) {
super(
'',
TelemetryKeys.AutoOAuth,
telemetryService,
layoutService,
clipboardService,
themeService,
logService,
textResourcePropertiesService,
contextKeyService,
{
isFlyout: true,
hasBackButton: true,
hasSpinner: true
}
);
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
this.backButton.onDidClick(() => this.cancel());
this._register(attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }));
this._copyAndOpenButton = this.addFooterButton(localize('copyAndOpen', "Copy & Open"), () => this.addAccount());
this._closeButton = this.addFooterButton(localize('oauthDialog.cancel', "Cancel"), () => this.cancel());
this.registerListeners();
this._userCodeInputBox.disable();
this._websiteInputBox.disable();
}
protected layout(height?: number): void {
// NO OP
}
protected renderBody(container: HTMLElement) {
const body = append(container, $('.auto-oauth-dialog'));
this._descriptionElement = append(body, $('.auto-oauth-description-section.new-section'));
const addAccountSection = append(body, $('.auto-oauth-info-section.new-section'));
this._userCodeInputBox = this.createInputBoxHelper(addAccountSection, localize('userCode', "User code"));
this._websiteInputBox = this.createInputBoxHelper(addAccountSection, localize('website', "Website"));
}
private createInputBoxHelper(container: HTMLElement, label: string): InputBox {
const inputContainer = append(container, $('.dialog-input-section'));
append(inputContainer, $('.dialog-label')).innerText = label;
const inputCellContainer = append(inputContainer, $('.dialog-input'));
return new InputBox(inputCellContainer, this._contextViewService, {
ariaLabel: label
});
}
private registerListeners(): void {
// Theme styler
this._register(attachButtonStyler(this._copyAndOpenButton, this._themeService));
this._register(attachButtonStyler(this._closeButton, this._themeService));
this._register(attachInputBoxStyler(this._userCodeInputBox, this._themeService));
this._register(attachInputBoxStyler(this._websiteInputBox, this._themeService));
}
/* Overwrite escape key behavior */
protected onClose() {
this.cancel();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.addAccount();
}
private addAccount() {
if (this._copyAndOpenButton.enabled) {
this._copyAndOpenButton.enabled = false;
this.spinner = true;
this._onHandleAddAccount.fire();
}
}
public cancel() {
this._onCancel.fire();
}
public close() {
this._copyAndOpenButton.enabled = true;
this._onCloseEvent.fire();
this.spinner = false;
this.hide();
}
public open(title: string, message: string, userCode: string, uri: string) {
// Update dialog
this.title = title;
this._descriptionElement.innerText = message;
this._userCodeInputBox.value = userCode;
this._websiteInputBox.value = uri;
this.show();
this._copyAndOpenButton.focus();
}
}

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import { AutoOAuthDialog } from 'sql/workbench/services/accountManagement/browser/autoOAuthDialog';
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
export class AutoOAuthDialogController {
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _autoOAuthDialog: AutoOAuthDialog;
private _providerId?: string;
private _userCode: string;
private _uri: string;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IAccountManagementService private _accountManagementService: IAccountManagementService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
) { }
/**
* Open auto OAuth dialog
*/
public openAutoOAuthDialog(providerId: string, title: string, message: string, userCode: string, uri: string): Thenable<void> {
if (this._providerId !== undefined) {
// If a oauth flyout is already open, return an error
const errorMessage = localize('oauthFlyoutIsAlreadyOpen', "Cannot start auto OAuth. An auto OAuth is already in progress.");
this._errorMessageService.showDialog(Severity.Error, '', errorMessage);
return Promise.reject(new Error('Auto OAuth dialog already open'));
}
// Create a new dialog if one doesn't exist
if (!this._autoOAuthDialog) {
this._autoOAuthDialog = this._instantiationService.createInstance(AutoOAuthDialog);
this._autoOAuthDialog.onHandleAddAccount(this.handleOnAddAccount, this);
this._autoOAuthDialog.onCancel(this.handleOnCancel, this);
this._autoOAuthDialog.onCloseEvent(this.handleOnClose, this);
this._autoOAuthDialog.render();
}
this._userCode = userCode;
this._uri = uri;
// Open the dialog
this._autoOAuthDialog.open(title, message, userCode, uri);
this._providerId = providerId;
return Promise.resolve();
}
/**
* Close auto OAuth dialog
*/
public closeAutoOAuthDialog(): void {
this._autoOAuthDialog.close();
this._providerId = undefined;
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private handleOnCancel(): void {
this._accountManagementService.cancelAutoOAuthDeviceCode(this._providerId!); // this should be always true
}
private handleOnClose(): void {
this._providerId = undefined;
}
private handleOnAddAccount(): void {
this._accountManagementService.copyUserCodeAndOpenBrowser(this._userCode, this._uri);
}
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Icons for various account actions */
.vs .action-item .codicon.add-linked-account-action {
background-image: url('new_account.svg');
}
.vs-dark .action-item .codicon.add-linked-account-action,
.hc-black .action-item .codicon.add-linked-account-action {
background-image: url('new_account_inverse.svg');
}

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.no-account-view {
font-size: 12px;
padding: 15px;
}
.no-account-view .no-account-view-label {
padding-bottom: 15px;
}
.account-view .header {
position: relative;
line-height: 22px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
padding-left: 20px;
padding-right: 12px;
overflow: hidden;
display: flex;
}
.account-view .header .title {
display: flex;
justify-content: flex-start;
flex: 1 1 auto;
}
.account-view .header .actions {
display: flex;
justify-content: flex-end;
}
.account-view .header .actions .action-item .action-label {
width: 30px;
background-size: 16px;
background-position: center center;
background-repeat: no-repeat;
margin-right: 0;
height: 22px;
}
.account-view .header .count-badge-wrapper {
justify-content: flex-end;
/* hide the count badge as it is not providing much value and meanwhile not keyboard accessible*/
display: none;
}
.account-view .list-row {
padding: 12px;
line-height: 18px !important;
}
.account-view .list-row .codicon {
flex: 0 0 50px;
height: 50px;
width: 50px;
background-size: 50px;
}
.account-view .list-row .codicon .badge {
position: absolute;
top: 43px;
left: 43px;
overflow: hidden;
width: 22px;
height: 22px;
}
.account-view .list-row .codicon .badge .badge-content {
width: 22px;
height: 22px;
background-size: 22px;
}
.account-view .list-row .actions-container {
flex: 0 0 50px;
}
.account-view .list-row .actions-container .action-item .action-label {
width: 16px;
margin-left: 20px;
margin-right: 10px;
background-size: 16px;
background-position: center;
background-repeat: no-repeat;
}
.account-view .list-row .actions-container .action-item .action-label.codicon.remove {
background-size: 14px !important;
}
.account-view .list-row .actions-container {
display: none;
}
.account-view .monaco-list .monaco-list-row:hover .list-row .actions-container,
.account-view .monaco-list .monaco-list-row.selected .list-row .actions-container,
.account-view .monaco-list .monaco-list-row.focused .list-row .actions-container{
display: block;
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.list-row.account-picker-list {
display: flex;
align-items: flex-start;
}
.list-row.account-picker-list .label {
flex: 1 1 auto;
margin-left: 15px;
}
.list-row.account-picker-list .label .contextual-display-name {
font-size: 15px;
line-height: 15px;
}
.list-row.account-picker-list .label .display-name {
font-size: 13px;
line-height: 13px;
}
.list-row.account-picker-list .label .content {
opacity: 0.7;
}
.account-logo {
background: no-repeat center center;
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Selected account */
.selected-account-container {
padding: 6px;
display: flex;
align-items: flex-start;
}
.selected-account-container .codicon {
flex: 0 0 25px;
height: 25px;
width: 25px;
}
.selected-account-container .label {
flex: 1 1 auto;
padding-left: 10px;
align-self: center;
}
.selected-account-container .codicon {
background-size: 25px;
}
.selected-account-container .codicon .badge {
position: relative;
top: 15px;
left: 15px;
overflow: hidden;
width: 12px;
height: 12px;
}
.selected-account-container .codicon .badge .badge-content {
width: 12px;
height: 12px;
background-size: 12px;
}
/* A container when the account list is empty */
.no-account-container {
padding: 6px;
opacity: 0.7;
font-style: italic;
}
/* Account list */
.account-list-container .list-row {
padding: 6px;
}
.account-list-container .list-row .codicon {
flex: 0 0 35px;
height: 35px;
width: 35px;
background-size: 35px;
}
.account-list-container .list-row .codicon .badge {
position: relative;
top: 22px;
left: 22px;
overflow: hidden;
width: 15px;
height: 15px;
}
.account-list-container .list-row .codicon .badge .badge-content {
width: 15px;
height: 15px;
background-size: 15px;
}
/* Refresh link */
.refresh-container {
padding-left: 15px;
padding-top: 6px;
display: flex;
align-items: flex-start;
}
.refresh-container .monaco-action-bar .actions-container {
justify-content: flex-start;
}
.refresh-container .codicon {
flex: 0 0 16px;
height: 16px;
width: 16px;
}
.refresh-container .monaco-action-bar {
flex: 1 1 auto;
margin-left: 10px;
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.auto-oauth-dialog {
padding: 15px
}
.modal .auto-oauth-dialog .new-section {
padding-bottom: 30px;
}
.modal .auto-oauth-dialog .dialog-input-section {
display: flex;
padding-left: 15px;
padding-right: 15px;
padding-top: 10px;
padding-bottom: 10px;
}
.modal .auto-oauth-dialog .dialog-input-section .dialog-label {
flex: 0 0 100px;
align-self: center;
}
.modal .auto-oauth-dialog .dialog-input-section .dialog-input {
flex: 1 1 auto;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>new_account_16x16</title><path d="M8.09,7.39a5.91,5.91,0,0,1,1.38.74,6.06,6.06,0,0,1,1.13,1.06A6,6,0,0,1,11.88,12h-1a4.94,4.94,0,0,0-.63-1.61A5,5,0,0,0,7.66,8.3a5,5,0,0,0-3-.12,5.09,5.09,0,0,0-1.2.5,5,5,0,0,0-1.79,1.79,5.07,5.07,0,0,0-.5,1.2A4.88,4.88,0,0,0,1,13H0a5.88,5.88,0,0,1,.28-1.8A6,6,0,0,1,1,9.59a6.1,6.1,0,0,1,1.22-1.3,5.8,5.8,0,0,1,1.59-.9A4.2,4.2,0,0,1,2.46,5.94,3.86,3.86,0,0,1,2,4a3.92,3.92,0,0,1,.31-1.56A4,4,0,0,1,4.4.31a4,4,0,0,1,3.12,0A4,4,0,0,1,9.65,2.44,4,4,0,0,1,9.83,5a4,4,0,0,1-1,1.74A3.94,3.94,0,0,1,8.09,7.39ZM3,4A2.92,2.92,0,0,0,3.2,5.17a3,3,0,0,0,1.6,1.6,3,3,0,0,0,2.33,0,3,3,0,0,0,1.6-1.6,3,3,0,0,0,0-2.33,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.92,2.92,0,0,0,3,4Zm13,9v1H14v2H13V14H11V13h2V11h1v2Z"/></svg>

After

Width:  |  Height:  |  Size: 850 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>new_account_inverse_16x16</title><path class="cls-1" d="M8.13,7.39a5.91,5.91,0,0,1,1.38.74,6.06,6.06,0,0,1,1.13,1.06A6,6,0,0,1,11.91,12h-1a4.94,4.94,0,0,0-.63-1.61A5,5,0,0,0,7.7,8.3a5,5,0,0,0-3-.12,5.09,5.09,0,0,0-1.2.5,5,5,0,0,0-1.79,1.79,5.07,5.07,0,0,0-.5,1.2A4.88,4.88,0,0,0,1,13H0a5.88,5.88,0,0,1,.28-1.8,6,6,0,0,1,.79-1.6,6.1,6.1,0,0,1,1.22-1.3,5.8,5.8,0,0,1,1.59-.9A4.2,4.2,0,0,1,2.5,5.94,3.86,3.86,0,0,1,2,4a3.92,3.92,0,0,1,.31-1.56A4,4,0,0,1,4.44.31a4,4,0,0,1,3.12,0A4,4,0,0,1,9.69,2.44,4,4,0,0,1,9.87,5a4,4,0,0,1-1,1.74A3.94,3.94,0,0,1,8.13,7.39ZM3,4a2.92,2.92,0,0,0,.23,1.17,3,3,0,0,0,1.6,1.6,3,3,0,0,0,2.33,0,3,3,0,0,0,1.6-1.6,3,3,0,0,0,0-2.33,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.92,2.92,0,0,0,3,4Zm13,9v1H14v2H13V14H11V13h2V11h1v2Z"/></svg>

After

Width:  |  Height:  |  Size: 918 B