move code from parts to contrib (#8319)

This commit is contained in:
Anthony Dresser
2019-11-14 12:23:11 -08:00
committed by GitHub
parent 6438967202
commit 7a2c30e159
619 changed files with 848 additions and 848 deletions

View File

@@ -0,0 +1,357 @@
/*---------------------------------------------------------------------------------------------
* 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 { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
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 { attachModalDialogStyler, attachButtonStyler, attachPanelStyler } 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/contrib/accounts/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/resourceConfiguration';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
class AccountPanel extends ViewletPanel {
public index: number;
private accountList: List<azdata.Account>;
constructor(
private options: IViewletPanelOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService private instantiationService: IInstantiationService,
@IThemeService private themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService);
}
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,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
) {
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-panel-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._instantiationService,
this._themeService,
this.contextKeyService
);
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/contrib/accounts/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,140 @@
/*---------------------------------------------------------------------------------------------
* 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 'vs/css!sql/media/icons/common-icons';
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

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom';
import * as resources from 'vs/base/common/resources';
export interface IAccountContrib {
id: string;
icon?: IUserFriendlyIcon;
}
export type IUserFriendlyIcon = string | { light: string; dark: string; };
const account: IJSONSchema = {
type: 'object',
properties: {
id: {
description: localize('carbon.extension.contributes.account.id', "Identifier of the account type"),
type: 'string'
},
icon: {
description: localize('carbon.extension.contributes.account.icon', "(Optional) Icon which is used to represent the accpunt in the UI. Either a file path or a themable configuration"),
anyOf: [{
type: 'string'
},
{
type: 'object',
properties: {
light: {
description: localize('carbon.extension.contributes.account.icon.light', "Icon path when a light theme is used"),
type: 'string'
},
dark: {
description: localize('carbon.extension.contributes.account.icon.dark', "Icon path when a dark theme is used"),
type: 'string'
}
}
}]
}
}
};
export const accountsContribution: IJSONSchema = {
description: localize('carbon.extension.contributes.account', "Contributes icons to account provider."),
oneOf: [
account,
{
type: 'array',
items: account
}
]
};
ExtensionsRegistry.registerExtensionPoint<IAccountContrib | IAccountContrib[]>({ extensionPoint: 'account-type', jsonSchema: accountsContribution }).setHandler(extensions => {
function handleCommand(account: IAccountContrib, extension: IExtensionPointUser<any>) {
const { icon, id } = account;
if (icon) {
const iconClass = id;
if (typeof icon === 'string') {
const path = resources.joinPath(extension.description.extensionLocation, icon);
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(path)}`);
} else {
const light = resources.joinPath(extension.description.extensionLocation, icon.light);
const dark = resources.joinPath(extension.description.extensionLocation, icon.dark);
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(light)}`);
createCSSRule(`.vs-dark .codicon.${iconClass}, .hc-black .codicon.${iconClass}`, `background-image: ${asCSSUrl(dark)}`);
}
}
}
for (let extension of extensions) {
const { value } = extension;
if (Array.isArray<IAccountContrib>(value)) {
for (let command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
});

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/contrib/accounts/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/contrib/accounts/browser/accountPicker';
import { AccountPicker } from 'sql/workbench/contrib/accounts/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,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar';
import { localize } from 'vs/nls';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
CommandsRegistry.registerCommand('workbench.actions.modal.linkedAccount', accessor => {
const accountManagementService = accessor.get(IAccountManagementService);
accountManagementService.openAccountListDialog();
});
class AccountsStatusBarContributions extends Disposable implements IWorkbenchContribution {
constructor(
@IStatusbarService private readonly statusbarService: IStatusbarService
) {
super();
this._register(
this.statusbarService.addEntry({
command: 'workbench.actions.modal.linkedAccount',
text: '$(person-filled)'
},
'status.accountList',
localize('status.problems', "Problems"),
StatusbarAlignment.LEFT, 15000 /* Highest Priority */)
);
}
}
workbenchRegistry.registerWorkbenchContribution(AccountsStatusBarContributions, LifecyclePhase.Restored);

View File

@@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* 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 { attachModalDialogStyler, 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/resourceConfiguration';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
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/contrib/accounts/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,305 @@
/*---------------------------------------------------------------------------------------------
* 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/firewallRuleDialog';
import * as DOM from 'vs/base/browser/dom';
import { Event, Emitter } from 'vs/base/common/event';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { localize } from 'vs/nls';
import { buttonBackground } from 'vs/platform/theme/common/colorRegistry';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { URI } from 'vs/base/common/uri';
import * as azdata from 'azdata';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { FirewallRuleViewModel } from 'sql/platform/accounts/common/firewallRuleViewModel';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/platform/theme/common/styler';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { IAccountPickerService } from 'sql/workbench/contrib/accounts/browser/accountPicker';
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/resourceConfiguration';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
// TODO: Make the help link 1) extensible (01/08/2018, https://github.com/Microsoft/azuredatastudio/issues/450)
// in case that other non-Azure sign in is to be used
const firewallHelpUri = 'https://aka.ms/sqlopsfirewallhelp';
const LocalizedStrings = {
FROM: localize('from', "From"),
TO: localize('to', "To")
};
export class FirewallRuleDialog extends Modal {
public viewModel: FirewallRuleViewModel;
private _createButton: Button;
private _closeButton: Button;
private _fromRangeinputBox: InputBox;
private _toRangeinputBox: InputBox;
private _helpLink: HTMLElement;
private _IPAddressInput: HTMLElement;
private _subnetIPRangeInput: HTMLElement;
private _IPAddressElement: HTMLElement;
// EVENTING ////////////////////////////////////////////////////////////
private _onAddAccountErrorEmitter: Emitter<string>;
public get onAddAccountErrorEvent(): Event<string> { return this._onAddAccountErrorEmitter.event; }
private _onCancel: Emitter<void>;
public get onCancel(): Event<void> { return this._onCancel.event; }
private _onCreateFirewallRule: Emitter<void>;
public get onCreateFirewallRule(): Event<void> { return this._onCreateFirewallRule.event; }
constructor(
@IAccountPickerService private _accountPickerService: IAccountPickerService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IThemeService themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IContextViewService private _contextViewService: IContextViewService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
@IOpenerService private readonly openerService: IOpenerService
) {
super(
localize('createNewFirewallRule', "Create new firewall rule"),
TelemetryKeys.FireWallRule,
telemetryService,
layoutService,
clipboardService,
themeService,
logService,
textResourcePropertiesService,
contextKeyService,
{
isFlyout: true,
hasBackButton: true,
hasSpinner: true
}
);
// Setup event emitters
this._onAddAccountErrorEmitter = new Emitter<string>();
this._onCancel = new Emitter<void>();
this._onCreateFirewallRule = new Emitter<void>();
this.viewModel = this._instantiationService.createInstance(FirewallRuleViewModel);
}
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._createButton = this.addFooterButton(localize('firewall.ok', "OK"), () => this.createFirewallRule());
this._closeButton = this.addFooterButton(localize('firewall.cancel', "Cancel"), () => this.cancel());
this.registerListeners();
}
protected renderBody(container: HTMLElement) {
const body = DOM.append(container, DOM.$('.firewall-rule-dialog'));
const descriptionSection = DOM.append(body, DOM.$('.firewall-rule-description-section.new-section'));
DOM.append(descriptionSection, DOM.$('div.firewall-rule-icon'));
const textDescriptionContainer = DOM.append(descriptionSection, DOM.$('div.firewall-rule-description'));
const dialogDescription = localize('firewallRuleDialogDescription',
"Your client IP address does not have access to the server. Sign in to an Azure account and create a new firewall rule to enable access.");
this.createLabelElement(textDescriptionContainer, dialogDescription, false);
this._helpLink = DOM.append(textDescriptionContainer, DOM.$('a.help-link'));
this._helpLink.setAttribute('href', firewallHelpUri);
this._helpLink.innerHTML += localize('firewallRuleHelpDescription', "Learn more about firewall settings");
this._helpLink.onclick = () => {
this.openerService.open(URI.parse(firewallHelpUri));
};
// Create account picker with event handling
this._accountPickerService.addAccountCompleteEvent(() => this.spinner = false);
this._accountPickerService.addAccountErrorEvent((msg) => {
this.spinner = false;
this._onAddAccountErrorEmitter.fire(msg);
});
this._accountPickerService.addAccountStartEvent(() => this.spinner = true);
this._accountPickerService.onAccountSelectionChangeEvent((account) => this.onAccountSelectionChange(account));
const azureAccountSection = DOM.append(body, DOM.$('.azure-account-section.new-section'));
const azureAccountLabel = localize('azureAccount', "Azure account");
this.createLabelElement(azureAccountSection, azureAccountLabel, true);
this._accountPickerService.renderAccountPicker(DOM.append(azureAccountSection, DOM.$('.dialog-input')));
const firewallRuleSection = DOM.append(body, DOM.$('.firewall-rule-section.new-section'));
const firewallRuleLabel = localize('filewallRule', "Firewall rule");
this.createLabelElement(firewallRuleSection, firewallRuleLabel, true);
const radioContainer = DOM.append(firewallRuleSection, DOM.$('.radio-section'));
const form = DOM.append(radioContainer, DOM.$('form.firewall-rule'));
const IPAddressDiv = DOM.append(form, DOM.$('div.firewall-ip-address dialog-input'));
const subnetIPRangeDiv = DOM.append(form, DOM.$('div.firewall-subnet-ip-range dialog-input'));
const IPAddressContainer = DOM.append(IPAddressDiv, DOM.$('div.option-container'));
this._IPAddressInput = DOM.append(IPAddressContainer, DOM.$('input.option-input'));
this._IPAddressInput.setAttribute('type', 'radio');
this._IPAddressInput.setAttribute('name', 'firewallRuleChoice');
this._IPAddressInput.setAttribute('value', 'ipAddress');
const IPAddressDescription = DOM.append(IPAddressContainer, DOM.$('div.option-description'));
IPAddressDescription.innerText = localize('addIPAddressLabel', "Add my client IP ");
this._IPAddressElement = DOM.append(IPAddressContainer, DOM.$('div.option-ip-address'));
const subnetIpRangeContainer = DOM.append(subnetIPRangeDiv, DOM.$('div.option-container'));
this._subnetIPRangeInput = DOM.append(subnetIpRangeContainer, DOM.$('input.option-input'));
this._subnetIPRangeInput.setAttribute('type', 'radio');
this._subnetIPRangeInput.setAttribute('name', 'firewallRuleChoice');
this._subnetIPRangeInput.setAttribute('value', 'ipRange');
const subnetIPRangeDescription = DOM.append(subnetIpRangeContainer, DOM.$('div.option-description'));
subnetIPRangeDescription.innerText = localize('addIpRangeLabel', "Add my subnet IP range");
const subnetIPRangeSection = DOM.append(subnetIPRangeDiv, DOM.$('.subnet-ip-range-input'));
const inputContainer = DOM.append(subnetIPRangeSection, DOM.$('.dialog-input-section'));
DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.FROM;
this._fromRangeinputBox = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, {
ariaLabel: LocalizedStrings.FROM
});
DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.TO;
this._toRangeinputBox = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, {
ariaLabel: LocalizedStrings.TO
});
this._register(this._themeService.onThemeChange(e => this.updateTheme(e)));
this.updateTheme(this._themeService.getTheme());
this._register(DOM.addDisposableListener(this._IPAddressElement, DOM.EventType.CLICK, () => {
this.onFirewallRuleOptionSelected(true);
}));
this._register(DOM.addDisposableListener(this._subnetIPRangeInput, DOM.EventType.CLICK, () => {
this.onFirewallRuleOptionSelected(false);
}));
}
private onFirewallRuleOptionSelected(isIPAddress: boolean) {
this.viewModel.isIPAddressSelected = isIPAddress;
if (this._fromRangeinputBox) {
isIPAddress ? this._fromRangeinputBox.disable() : this._fromRangeinputBox.enable();
}
if (this._toRangeinputBox) {
isIPAddress ? this._toRangeinputBox.disable() : this._toRangeinputBox.enable();
}
}
protected layout(height?: number): void {
// Nothing currently laid out statically in this class
}
private createLabelElement(container: HTMLElement, content: string, isHeader?: boolean) {
let className = 'dialog-label';
if (isHeader) {
className += ' header';
}
DOM.append(container, DOM.$(`.${className}`)).innerText = content;
}
// Update theming that is specific to firewall rule flyout body
private updateTheme(theme: ITheme): void {
const linkColor = theme.getColor(buttonBackground);
const link = linkColor ? linkColor.toString() : null;
if (this._helpLink) {
this._helpLink.style.color = link;
}
}
private registerListeners(): void {
// Theme styler
this._register(attachButtonStyler(this._createButton, this._themeService));
this._register(attachButtonStyler(this._closeButton, this._themeService));
this._register(attachInputBoxStyler(this._fromRangeinputBox, this._themeService));
this._register(attachInputBoxStyler(this._toRangeinputBox, this._themeService));
// handler for from subnet ip range change events
this._register(this._fromRangeinputBox.onDidChange(IPAddress => {
this.fromRangeInputChanged(IPAddress);
}));
// handler for to subnet ip range change events
this._register(this._toRangeinputBox.onDidChange(IPAddress => {
this.toRangeInputChanged(IPAddress);
}));
}
private fromRangeInputChanged(IPAddress: string) {
this.viewModel.fromSubnetIPRange = IPAddress;
}
private toRangeInputChanged(IPAddress: string) {
this.viewModel.toSubnetIPRange = IPAddress;
}
/* Overwrite esapce key behavior */
protected onClose() {
this.cancel();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.createFirewallRule();
}
public cancel() {
this._onCancel.fire();
this.close();
}
public close() {
this.hide();
}
public createFirewallRule() {
if (this._createButton.enabled) {
this._createButton.enabled = false;
this.spinner = true;
this._onCreateFirewallRule.fire();
}
}
public onAccountSelectionChange(account: azdata.Account | undefined): void {
this.viewModel.selectedAccount = account;
if (account && !account.isStale) {
this._createButton.enabled = true;
} else {
this._createButton.enabled = false;
}
}
public onServiceComplete() {
this._createButton.enabled = true;
this.spinner = false;
}
public open() {
this._IPAddressInput.click();
this.onAccountSelectionChange(this._accountPickerService.selectedAccount);
this._fromRangeinputBox.setPlaceHolder(this.viewModel.defaultFromSubnetIPRange);
this._toRangeinputBox.setPlaceHolder(this.viewModel.defaultToSubnetIPRange);
this._IPAddressElement.innerText = '(' + this.viewModel.defaultIPAddress + ')';
this.show();
}
}

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* 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 * as azdata from 'azdata';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { FirewallRuleDialog } from 'sql/workbench/contrib/accounts/browser/firewallRuleDialog';
import { IAccountManagementService, AzureResource } from 'sql/platform/accounts/common/interfaces';
import { IResourceProviderService } from 'sql/workbench/services/resourceProvider/common/resourceProviderService';
import { Deferred } from 'sql/base/common/promise';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
export class FirewallRuleDialogController {
private _firewallRuleDialog: FirewallRuleDialog;
private _connection: IConnectionProfile;
private _resourceProviderId: string;
private _addAccountErrorTitle = localize('firewallDialog.addAccountErrorTitle', "Error adding account");
private _firewallRuleErrorTitle = localize('firewallRuleError', "Firewall rule error");
private _deferredPromise: Deferred<boolean>;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
@IAccountManagementService private _accountManagementService: IAccountManagementService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
) {
}
/**
* Open firewall rule dialog
*/
public openFirewallRuleDialog(connection: IConnectionProfile, ipAddress: string, resourceProviderId: string): Promise<boolean> {
if (!this._firewallRuleDialog) {
this._firewallRuleDialog = this._instantiationService.createInstance(FirewallRuleDialog);
this._firewallRuleDialog.onCancel(this.handleOnCancel, this);
this._firewallRuleDialog.onCreateFirewallRule(this.handleOnCreateFirewallRule, this);
this._firewallRuleDialog.onAddAccountErrorEvent(this.handleOnAddAccountError, this);
this._firewallRuleDialog.render();
}
this._connection = connection;
this._resourceProviderId = resourceProviderId;
this._firewallRuleDialog.viewModel.updateDefaultValues(ipAddress);
this._firewallRuleDialog.open();
this._deferredPromise = new Deferred();
return this._deferredPromise.promise;
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private handleOnAddAccountError(message: string): void {
this._errorMessageService.showDialog(Severity.Error, this._addAccountErrorTitle, message);
}
private async handleOnCreateFirewallRule(): Promise<void> {
const resourceProviderId = this._resourceProviderId;
try {
const securityTokenMappings = await this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount!, AzureResource.ResourceManagement);
const firewallRuleInfo: azdata.FirewallRuleInfo = {
startIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.fromSubnetIPRange,
endIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.toSubnetIPRange,
serverName: this._connection.serverName,
securityTokenMappings
};
const response = await this._resourceProviderService.createFirewallRule(this._firewallRuleDialog.viewModel.selectedAccount!, firewallRuleInfo, resourceProviderId);
if (response.result) {
this._firewallRuleDialog.close();
this._deferredPromise.resolve(true);
} else {
this._errorMessageService.showDialog(Severity.Error, this._firewallRuleErrorTitle, response.errorMessage);
}
this._firewallRuleDialog.onServiceComplete();
} catch (e) {
this.showError(e);
}
}
private showError(error: any): void {
this._errorMessageService.showDialog(Severity.Error, this._firewallRuleErrorTitle, error);
this._firewallRuleDialog.onServiceComplete();
// Note: intentionally not rejecting the promise as we want users to be able to choose a different account
}
private handleOnCancel(): void {
this._deferredPromise.resolve(false);
}
}

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,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.statusbar-item .linked-account-staus a.linked-account-status-selection .linked-account-icon {
-webkit-mask: url('accounts_statusbar_inverse.svg') no-repeat 50% 50%;
-webkit-mask-size: 12px;
width: 12px;
height: 22px;
}
.statusbar-item .linked-account-staus a.linked-account-status-selection {
padding: 0 5px 0 5px;
}

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 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 10.89"><defs><style>.cls-1{fill:#fff;}</style></defs><title>accounts_statusbar_inverse_11x11</title><path class="cls-1" d="M10.08,5.66a3.44,3.44,0,0,0-.34-.75,3.47,3.47,0,0,0-.52-.64A3.23,3.23,0,0,0,8.8,4a2.28,2.28,0,0,0,.7-1.65,2.27,2.27,0,0,0-.18-.89A2.31,2.31,0,0,0,8.09.19,2.19,2.19,0,0,0,7.19,0,2.16,2.16,0,0,0,6.3.19a2.41,2.41,0,0,0-.73.49,2.41,2.41,0,0,0-.49.73,2.16,2.16,0,0,0-.19.89,2.22,2.22,0,0,0,.25,1A2.31,2.31,0,0,0,5.58,4a3.62,3.62,0,0,0-.52.41,3.62,3.62,0,0,0-.41.52A2.31,2.31,0,0,0,4,4.44a2.26,2.26,0,0,0-1-.25,2.16,2.16,0,0,0-.89.19,2.41,2.41,0,0,0-.73.49,2.41,2.41,0,0,0-.49.73,2.16,2.16,0,0,0-.19.89,2.22,2.22,0,0,0,.25,1,2.31,2.31,0,0,0,.45.61A3.7,3.7,0,0,0,1,8.47a3.47,3.47,0,0,0-.52.64,2.88,2.88,0,0,0-.34.75,3.06,3.06,0,0,0-.12.83v.21H1.12v-.21A1.83,1.83,0,0,1,1.27,10a1.9,1.9,0,0,1,1-1A1.83,1.83,0,0,1,3,8.8a1.86,1.86,0,0,1,.73.15,2,2,0,0,1,.6.4,1.9,1.9,0,0,1,.4.6,1.85,1.85,0,0,1,.15.73v.21H6v-.21a3.05,3.05,0,0,0-.11-.83,3.44,3.44,0,0,0-.34-.75A3.25,3.25,0,0,0,5,8.47a3.23,3.23,0,0,0-.43-.32,2.28,2.28,0,0,0,.7-1.65,1.83,1.83,0,0,1,.15-.73,1.9,1.9,0,0,1,1-1,1.83,1.83,0,0,1,.73-.15,1.86,1.86,0,0,1,.73.15,1.9,1.9,0,0,1,1,1,1.82,1.82,0,0,1,.15.73V6.7h1.12V6.49A3.05,3.05,0,0,0,10.08,5.66ZM4.1,7a1.24,1.24,0,0,1-.64.64A1.13,1.13,0,0,1,3,7.68a1.1,1.1,0,0,1-.45-.09,1.21,1.21,0,0,1-.38-.26A1.1,1.1,0,0,1,1.91,7a1.11,1.11,0,0,1-.1-.46A1.13,1.13,0,0,1,1.91,6a1.1,1.1,0,0,1,.26-.38,1.1,1.1,0,0,1,.38-.26A1.1,1.1,0,0,1,3,5.31a1.16,1.16,0,0,1,.46.1A1.24,1.24,0,0,1,4.1,6a1.15,1.15,0,0,1,.09.45A1.13,1.13,0,0,1,4.1,7ZM8.29,2.76a1.24,1.24,0,0,1-.64.64,1.13,1.13,0,0,1-.46.09,1.1,1.1,0,0,1-.45-.09,1.21,1.21,0,0,1-.38-.26,1.1,1.1,0,0,1-.26-.38A1.14,1.14,0,0,1,6,2.3a1.15,1.15,0,0,1,.1-.45,1.1,1.1,0,0,1,.26-.38,1.19,1.19,0,0,1,.83-.36,1.16,1.16,0,0,1,.46.1A1.1,1.1,0,0,1,8,1.47a1.21,1.21,0,0,1,.26.38,1.15,1.15,0,0,1,.09.45A1.13,1.13,0,0,1,8.29,2.76Z"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

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,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.firewall-rule-dialog {
padding: 15px
}
.modal .firewall-rule-dialog .dialog-label.header {
font-size: 15px;
}
.modal .firewall-rule-dialog .new-section {
padding-bottom: 30px;
}
.modal .firewall-rule-dialog .dialog-input {
padding-left: 15px;
padding-right: 15px;
padding-top: 10px;
}
.modal .firewall-rule-dialog .dialog-input-section {
display: flex;
padding-left: 30px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.modal .firewall-rule-dialog .dialog-input-section .dialog-label {
flex: 0 0 15px;
align-self: center;
}
.modal .firewall-rule-dialog .dialog-input-section .dialog-input {
flex: 0 0 100px;
padding-left: 10px;
padding-right: 10px;
padding-top: 0px;
padding-bottom: 0px;
}
.modal .firewall-rule-dialog .azure-account-section {
height: 92px;
}
/* Firewall rule description section */
.modal .firewall-rule-dialog a:link {
text-decoration: underline;
}
.modal .firewall-rule-dialog .firewall-rule-description-section {
display: flex;
}
.modal .firewall-rule-dialog .firewall-rule-description-section .firewall-rule-icon {
background: url('secure.svg') center center no-repeat;
width: 50px;
height: 50px;
flex: 0 0 50px;
}
.modal .firewall-rule-dialog .firewall-rule-description-section .firewall-rule-description {
padding-left: 15px;
}
/* Radio button section*/
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container {
display: flex;
align-items: flex-start;
}
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-input {
margin-top: 0px;
align-self: center;
}
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-description {
padding-left: 10px;
}
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-ip-address {
padding-left: 3px;
}

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

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36.65 50"><defs><style>.cls-1{fill:#7fba00;}.cls-2{fill:#b8d432;opacity:0.4;isolation:isolate;}.cls-3{fill:#fff;}</style></defs><title>secure</title><path class="cls-1" d="M31.19,6h0c-11-1.22-12.86-6-12.86-6S15.85,6.24,0,6.24V31.85c0,3.1,1.72,6,4.1,8.53h0C9.51,46.13,18.33,50,18.33,50s18.33-8,18.33-18.15V6.24A49.77,49.77,0,0,1,31.19,6Z"/><path class="cls-2" d="M22.86,16.54,31.19,6c-11-1.22-12.86-6-12.86-6S15.85,6.24,0,6.24V31.85c0,3.1,1.72,6,4.1,8.53l6.15-7.82Z"/><path class="cls-3" d="M25.6,24.46H24.53V20.91a6.7,6.7,0,0,0-1.67-4.45h0l-.11-.13a6,6,0,0,0-8.84,0,6.7,6.7,0,0,0-1.78,4.58v3.55H11.06a.8.8,0,0,0-.8.8v7.22h0v2.17a.8.8,0,0,0,.8.8H25.6a.8.8,0,0,0,.8-.8V25.26A.8.8,0,0,0,25.6,24.46Zm-4,0H15.07V20.91a3.75,3.75,0,0,1,1-2.57,3,3,0,0,1,4.54,0,3.67,3.67,0,0,1,.39.5h0a3.8,3.8,0,0,1,.6,2.06Z"/></svg>

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import { Emitter } from 'vs/base/common/event';
import { AccountDialog } from 'sql/workbench/contrib/accounts/browser/accountDialog';
import { AccountDialogController } from 'sql/workbench/contrib/accounts/browser/accountDialogController';
import { AccountViewModel } from 'sql/platform/accounts/common/accountViewModel';
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { AccountListRenderer } from 'sql/workbench/contrib/accounts/browser/accountListRenderer';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
// TESTS ///////////////////////////////////////////////////////////////////
suite('Account Management Dialog Controller Tests', () => {
test('Open Account Dialog - Dialog Doesn\'t Exist', () => {
// Setup: Create instance of the controller
let instantiationService = createInstantiationService();
let controller = new AccountDialogController(instantiationService, undefined!);
assert.strictEqual(controller.accountDialog, undefined);
// If: I open the account dialog when one hasn't been opened
controller.openAccountDialog();
// Then:
// ... The account dialog should be defined
assert.notStrictEqual(controller.accountDialog, undefined);
});
test('Open Account Dialog - Dialog Exists', () => {
// Setup: Create instance of the controller with an account dialog already loaded
let instantiationService = createInstantiationService();
let controller = new AccountDialogController(instantiationService, undefined!);
controller.openAccountDialog();
let accountDialog = controller.accountDialog;
// If: I open the account dialog when one has already been opened
controller.openAccountDialog();
// Then: It should be the same dialog that already existed
assert.equal(controller.accountDialog, accountDialog);
});
test('Add Account Failure - Error Message Shown', () => {
// Setup:
// ... Create instantiation service that returns mock emitter for account dialog
let mockEventEmitter = new Emitter<string>();
let instantiationService = createInstantiationService(mockEventEmitter);
// ... Create a mock instance of the error message service
let errorMessageServiceStub = new TestErrorMessageService();
let mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
// ... Create instance of the controller with an opened dialog
let controller = new AccountDialogController(instantiationService, mockErrorMessageService.object);
controller.openAccountDialog();
// If: The account dialog reports a failure adding an account
mockEventEmitter.fire('Error message');
// Then: An error dialog should have been opened
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
});
});
function createInstantiationService(addAccountFailureEmitter?: Emitter<string>): InstantiationService {
// Create a mock account dialog view model
let accountViewModel = new AccountViewModel(new TestAccountManagementService());
let mockAccountViewModel = TypeMoq.Mock.ofInstance(accountViewModel);
let mockEvent = new Emitter<any>();
mockAccountViewModel.setup(x => x.addProviderEvent).returns(() => mockEvent.event);
mockAccountViewModel.setup(x => x.removeProviderEvent).returns(() => mockEvent.event);
mockAccountViewModel.setup(x => x.updateAccountListEvent).returns(() => mockEvent.event);
mockAccountViewModel.setup(x => x.initialize()).returns(() => Promise.resolve([]));
// Create a mocked out instantiation service
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountViewModel)))
.returns(() => mockAccountViewModel.object);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountListRenderer)))
.returns(() => undefined);
// Create a mock account dialog
let accountDialog = new AccountDialog(undefined!, undefined!, instantiationService.object, undefined!, undefined!, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!);
let mockAccountDialog = TypeMoq.Mock.ofInstance(accountDialog);
mockAccountDialog.setup(x => x.onAddAccountErrorEvent)
.returns(() => { return addAccountFailureEmitter ? addAccountFailureEmitter.event : mockEvent.event; });
mockAccountDialog.setup(x => x.onCloseEvent)
.returns(() => mockEvent.event);
mockAccountDialog.setup(x => x.render())
.returns(() => undefined);
mockAccountDialog.setup(x => x.open())
.returns(() => undefined);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountDialog)))
.returns(() => mockAccountDialog.object);
return instantiationService.object;
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import { EventVerifierSingle } from 'sql/base/test/common/event';
import { Emitter } from 'vs/base/common/event';
import { AccountPicker } from 'sql/workbench/contrib/accounts/browser/accountPickerImpl';
import { AccountPickerService } from 'sql/workbench/contrib/accounts/browser/accountPickerService';
import { AccountPickerViewModel } from 'sql/platform/accounts/common/accountPickerViewModel';
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
// SUITE STATE /////////////////////////////////////////////////////////////
let mockAddAccountCompleteEmitter: Emitter<void>;
let mockAddAccountErrorEmitter: Emitter<string>;
let mockAddAccountStartEmitter: Emitter<void>;
let mockOnAccountSelectionChangeEvent: Emitter<azdata.Account>;
// TESTS ///////////////////////////////////////////////////////////////////
suite('Account picker service tests', () => {
setup(() => {
// Setup event mocks for the account picker service
mockAddAccountCompleteEmitter = new Emitter<void>();
mockAddAccountErrorEmitter = new Emitter<string>();
mockAddAccountStartEmitter = new Emitter<void>();
mockOnAccountSelectionChangeEvent = new Emitter<azdata.Account>();
});
test('Construction - Events are properly defined', () => {
// Setup:
// ... Create instantiation service
let instantiationService = createInstantiationService();
// ... Create instance of the service and reder account picker
let service = new AccountPickerService(instantiationService);
service.renderAccountPicker(TypeMoq.It.isAny());
// Then:
// ... All the events for the view models should be properly initialized
assert.notEqual(service.addAccountCompleteEvent, undefined);
assert.notEqual(service.addAccountErrorEvent, undefined);
assert.notEqual(service.addAccountStartEvent, undefined);
assert.notEqual(service.onAccountSelectionChangeEvent, undefined);
// ... All the events should properly fire
let evAddAccountCompleteEvent = new EventVerifierSingle<void>();
service.addAccountCompleteEvent(evAddAccountCompleteEvent.eventHandler);
mockAddAccountCompleteEmitter.fire();
evAddAccountCompleteEvent.assertFired();
let errorMsg = 'Error';
let evAddAccountErrorEvent = new EventVerifierSingle<string>();
service.addAccountErrorEvent(evAddAccountErrorEvent.eventHandler);
mockAddAccountErrorEmitter.fire(errorMsg);
evAddAccountErrorEvent.assertFired(errorMsg);
let evAddAccountStartEvent = new EventVerifierSingle<void>();
service.addAccountStartEvent(evAddAccountStartEvent.eventHandler);
mockAddAccountStartEmitter.fire();
evAddAccountStartEvent.assertFired();
let account = {
key: { providerId: 'azure', accountId: 'account1' },
name: 'Account 1',
displayInfo: {
contextualDisplayName: 'Microsoft Account',
accountType: 'microsoft',
displayName: 'Account 1',
userId: 'user@email.com'
},
properties: [],
isStale: false
};
let evOnAccountSelectionChangeEvent = new EventVerifierSingle<azdata.Account>();
service.onAccountSelectionChangeEvent(evOnAccountSelectionChangeEvent.eventHandler);
mockOnAccountSelectionChangeEvent.fire(account);
evOnAccountSelectionChangeEvent.assertFired(account);
});
});
function createInstantiationService(): InstantiationService {
// Create a mock account picker view model
let providerId = 'azure';
let accountPickerViewModel = new AccountPickerViewModel(providerId, new TestAccountManagementService());
let mockAccountViewModel = TypeMoq.Mock.ofInstance(accountPickerViewModel);
let mockEvent = new Emitter<any>();
mockAccountViewModel.setup(x => x.updateAccountListEvent).returns(() => mockEvent.event);
// Create a mocked out instantiation service
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountPickerViewModel), TypeMoq.It.isAny()))
.returns(() => mockAccountViewModel.object);
// Create a mock account picker
let accountPicker = new AccountPicker('provider', new TestThemeService(), instantiationService.object, undefined!);
let mockAccountDialog = TypeMoq.Mock.ofInstance(accountPicker);
mockAccountDialog.setup(x => x.addAccountCompleteEvent)
.returns(() => mockAddAccountCompleteEmitter.event);
mockAccountDialog.setup(x => x.addAccountErrorEvent)
.returns((msg) => mockAddAccountErrorEmitter.event);
mockAccountDialog.setup(x => x.addAccountStartEvent)
.returns(() => mockAddAccountStartEmitter.event);
mockAccountDialog.setup(x => x.onAccountSelectionChangeEvent)
.returns((account) => mockOnAccountSelectionChangeEvent.event);
mockAccountDialog.setup(x => x.render(TypeMoq.It.isAny()))
.returns((container) => undefined);
mockAccountDialog.setup(x => x.createAccountPickerComponent());
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountPicker), TypeMoq.It.isAny()))
.returns(() => mockAccountDialog.object);
return instantiationService.object;
}

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 * as TypeMoq from 'typemoq';
import { Emitter } from 'vs/base/common/event';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { AutoOAuthDialog } from 'sql/workbench/contrib/accounts/browser/autoOAuthDialog';
import { AutoOAuthDialogController } from 'sql/workbench/contrib/accounts/browser/autoOAuthDialogController';
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
// TESTS ///////////////////////////////////////////////////////////////////
suite('auto OAuth dialog controller tests', () => {
let instantiationService: TypeMoq.Mock<InstantiationService>;
let mockAutoOAuthDialog: TypeMoq.Mock<AutoOAuthDialog>;
let mockAccountManagementService: TypeMoq.Mock<TestAccountManagementService>;
let mockErrorMessageService: TypeMoq.Mock<TestErrorMessageService>;
let autoOAuthDialogController: AutoOAuthDialogController;
let mockOnCancelEvent: Emitter<void>;
let mockOnAddAccountEvent: Emitter<void>;
let mockOnCloseEvent: Emitter<void>;
let providerId = 'azure';
let title = 'Add Account';
let message = 'This is the dialog description';
let userCode = 'abcde';
let uri = 'uri';
setup(() => {
mockOnCancelEvent = new Emitter<void>();
mockOnAddAccountEvent = new Emitter<void>();
mockOnCloseEvent = new Emitter<void>();
// Create a mock auto OAuth dialog
let autoOAuthDialog = new AutoOAuthDialog(undefined!, undefined!, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!);
mockAutoOAuthDialog = TypeMoq.Mock.ofInstance(autoOAuthDialog);
mockAutoOAuthDialog.setup(x => x.onCancel).returns(() => mockOnCancelEvent.event);
mockAutoOAuthDialog.setup(x => x.onHandleAddAccount).returns(() => mockOnAddAccountEvent.event);
mockAutoOAuthDialog.setup(x => x.onCloseEvent).returns(() => mockOnCloseEvent.event);
mockAutoOAuthDialog.setup(x => x.render());
mockAutoOAuthDialog.setup(x => x.open(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
mockAutoOAuthDialog.setup(x => x.close()).callback(() => {
mockOnCloseEvent.fire();
});
// Create a mocked out instantiation service
instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AutoOAuthDialog)))
.returns(() => mockAutoOAuthDialog.object);
// Create a mocked account management service
let accountManagementTestService = new TestAccountManagementService();
mockAccountManagementService = TypeMoq.Mock.ofInstance(accountManagementTestService);
mockAccountManagementService.setup(x => x.copyUserCodeAndOpenBrowser(TypeMoq.It.isAny(), TypeMoq.It.isAny()));
// Create a mocked error message service
let errorMessageServiceStub = new TestErrorMessageService();
mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
// Create a mocked auto OAuth dialog controller
autoOAuthDialogController = new AutoOAuthDialogController(instantiationService.object, mockAccountManagementService.object, mockErrorMessageService.object);
});
test('Open auto OAuth when the flyout is already open, return an error', (done) => {
// If: Open auto OAuth dialog first time
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
// Then: It should open the flyout successfully
mockAutoOAuthDialog.verify(x => x.open(title, message, userCode, uri), TypeMoq.Times.once());
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
// If: a oauth flyout is already open
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri)
.then(success => done('Failure: Expected error on 2nd dialog open'), error => done());
// Then: An error dialog should have been opened
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
});
test('Close auto OAuth dialog successfully', () => {
let title = 'Add Account';
let message = 'This is the dialog description';
let userCode = 'abcde';
let uri = 'uri';
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
// If: closeAutoOAuthDialog is called
autoOAuthDialogController.closeAutoOAuthDialog();
// Then: it should close the dialog
mockAutoOAuthDialog.verify(x => x.close(), TypeMoq.Times.once());
});
test('Open and close auto OAuth dialog multiple times should work properly', () => {
let title = 'Add Account';
let message = 'This is the dialog description';
let userCode = 'abcde';
let uri = 'uri';
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
autoOAuthDialogController.closeAutoOAuthDialog();
// If: Open the flyout second time
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
// Then: It should open the flyout twice successfully
mockAutoOAuthDialog.verify(x => x.open(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(2));
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
});
test('Copy and open button in auto OAuth dialog should work properly', () => {
let title = 'Add Account';
let message = 'This is the dialog description';
let userCode = 'abcde';
let uri = 'uri';
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
// If: the 'copy & open' button in auto Oauth dialog is selected
mockOnAddAccountEvent.fire();
// Then: copyUserCodeAndOpenBrowser should get called
mockAccountManagementService.verify(x => x.copyUserCodeAndOpenBrowser(userCode, uri), TypeMoq.Times.once());
});
// TODO: Test for cancel button
});

View File

@@ -0,0 +1,249 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as TypeMoq from 'typemoq';
import { Emitter } from 'vs/base/common/event';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { FirewallRuleDialog } from 'sql/workbench/contrib/accounts/browser/firewallRuleDialog';
import { FirewallRuleViewModel } from 'sql/platform/accounts/common/firewallRuleViewModel';
import { FirewallRuleDialogController } from 'sql/workbench/contrib/accounts/browser/firewallRuleDialogController';
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
import { TestResourceProvider } from 'sql/workbench/services/resourceProvider/test/common/testResourceProviderService';
import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { Deferred } from 'sql/base/common/promise';
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
// TESTS ///////////////////////////////////////////////////////////////////
suite('Firewall rule dialog controller tests', () => {
let connectionProfile: IConnectionProfile;
let account: azdata.Account;
let IPAddress = '250.222.155.198';
let mockOnAddAccountErrorEvent: Emitter<string>;
let mockOnCreateFirewallRule: Emitter<void>;
let instantiationService: TypeMoq.Mock<InstantiationService>;
let mockFirewallRuleViewModel: TypeMoq.Mock<FirewallRuleViewModel>;
let mockFirewallRuleDialog: TypeMoq.Mock<FirewallRuleDialog>;
setup(() => {
account = {
key: { providerId: 'azure', accountId: 'account1' },
displayInfo: {
contextualDisplayName: 'Microsoft Account',
accountType: 'microsoft',
displayName: 'Account 1',
userId: 'user@email.com'
},
properties: [],
isStale: false
};
mockOnAddAccountErrorEvent = new Emitter<string>();
mockOnCreateFirewallRule = new Emitter<void>();
// Create a mock firewall rule view model
let firewallRuleViewModel = new FirewallRuleViewModel();
mockFirewallRuleViewModel = TypeMoq.Mock.ofInstance(firewallRuleViewModel);
mockFirewallRuleViewModel.setup(x => x.updateDefaultValues(TypeMoq.It.isAny()))
.returns((ipAddress) => undefined);
mockFirewallRuleViewModel.object.selectedAccount = account;
mockFirewallRuleViewModel.object.isIPAddressSelected = true;
// Create a mocked out instantiation service
instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(FirewallRuleViewModel)))
.returns(() => mockFirewallRuleViewModel.object);
// Create a mock account picker
let firewallRuleDialog = new FirewallRuleDialog(undefined!, undefined!, undefined!, instantiationService.object, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!, undefined!);
mockFirewallRuleDialog = TypeMoq.Mock.ofInstance(firewallRuleDialog);
let mockEvent = new Emitter<any>();
mockFirewallRuleDialog.setup(x => x.onCancel)
.returns(() => mockEvent.event);
mockFirewallRuleDialog.setup(x => x.onCreateFirewallRule)
.returns(() => mockOnCreateFirewallRule.event);
mockFirewallRuleDialog.setup(x => x.onAddAccountErrorEvent)
.returns((msg) => mockOnAddAccountErrorEvent.event);
mockFirewallRuleDialog.setup(x => x.render());
mockFirewallRuleDialog.setup(x => x.open());
mockFirewallRuleDialog.setup(x => x.close());
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(FirewallRuleDialog)))
.returns(() => mockFirewallRuleDialog.object);
connectionProfile = {
connectionName: 'new name',
serverName: 'new server',
databaseName: 'database',
userName: 'user',
password: 'password',
authenticationType: '',
savePassword: true,
groupFullName: 'g2/g2-2',
groupId: 'group id',
getOptionsKey: () => '',
matches: () => false,
providerName: mssqlProviderName,
options: {},
saveProfile: true,
id: ''
};
});
test('Add Account Failure - Error Message Shown', () => {
// ... Create a mock instance of the error message service
let errorMessageServiceStub = new TestErrorMessageService();
let mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
// ... Create instance of the controller with an opened dialog
let controller = new FirewallRuleDialogController(instantiationService.object, undefined!, undefined!, mockErrorMessageService.object);
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
// If: The firewall rule dialog reports a failure
mockOnAddAccountErrorEvent.fire('Error message');
// Then: An error dialog should have been opened
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
});
test('create firewall rule success', async () => {
let deferredPromise = new Deferred();
mockFirewallRuleDialog.setup(x => x.onServiceComplete())
.callback(() => {
deferredPromise.resolve(true);
});
// ... Create a mock instance of the account management test service
let mockAccountManagementService = getMockAccountManagementService(true);
// ... Create a mock instance of the resource provider
let mockResourceProvider = getMockResourceProvider(true, { result: true, errorMessage: '' });
// ... Create instance of the controller with an opened dialog
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, undefined!);
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
// If: The firewall rule dialog's create firewall rule get fired
mockOnCreateFirewallRule.fire();
// Then: it should get security token from account management service and call create firewall rule in resource provider
await deferredPromise;
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockFirewallRuleDialog.verify(x => x.close(), TypeMoq.Times.once());
mockFirewallRuleDialog.verify(x => x.onServiceComplete(), TypeMoq.Times.once());
});
test('create firewall rule fails during getSecurity', async () => {
let deferredPromise = new Deferred<{}>();
// ... Create a mock instance of the error message service
let mockErrorMessageService = getMockErrorMessageService(deferredPromise);
// ... Create a mock instance of the account management test service
let mockAccountManagementService = getMockAccountManagementService(false);
// ... Create a mock instance of the resource provider
let mockResourceProvider = getMockResourceProvider(true, { result: true, errorMessage: '' });
// ... Create instance of the controller with an opened dialog
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, mockErrorMessageService.object);
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
// If: The firewall rule dialog's create firewall rule get fired
mockOnCreateFirewallRule.fire();
// Then: it should get security token from account management service and an error dialog should have been opened
await deferredPromise;
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
});
test('create firewall rule fails during createFirewallRule in ResourceProvider - result is false', async () => {
let deferredPromise = new Deferred<{}>();
// ... Create a mock instance of the error message service
let mockErrorMessageService = getMockErrorMessageService(deferredPromise);
// ... Create a mock instance of the account management test service
let mockAccountManagementService = getMockAccountManagementService(true);
// ... Create a mock instance of the resource provider
let mockResourceProvider = getMockResourceProvider(true, { result: false, errorMessage: '' });
// ... Create instance of the controller with an opened dialog
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, mockErrorMessageService.object);
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
// If: The firewall rule dialog's create firewall rule get fired
mockOnCreateFirewallRule.fire();
// Then: it should get security token from account management service and an error dialog should have been opened
await deferredPromise;
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
});
test('create firewall rule fails during createFirewallRule in ResourceProvider - reject promise', async () => {
let deferredPromise = new Deferred<{}>();
// ... Create a mock instance of the error message service
let mockErrorMessageService = getMockErrorMessageService(deferredPromise);
// ... Create a mock instance of the account management test service
let mockAccountManagementService = getMockAccountManagementService(true);
// ... Create a mock instance of the resource provider
let mockResourceProvider = getMockResourceProvider(false);
// ... Create instance of the controller with an opened dialog
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, mockErrorMessageService.object);
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
// If: The firewall rule dialog's create firewall rule get fired
mockOnCreateFirewallRule.fire();
// Then: it should get security token from account management service and an error dialog should have been opened
await deferredPromise;
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
});
});
function getMockAccountManagementService(resolveSecurityToken: boolean): TypeMoq.Mock<TestAccountManagementService> {
let accountManagementTestService = new TestAccountManagementService();
let mockAccountManagementService = TypeMoq.Mock.ofInstance(accountManagementTestService);
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => resolveSecurityToken ? Promise.resolve({}) : Promise.reject(null));
return mockAccountManagementService;
}
function getMockResourceProvider(resolveCreateFirewallRule: boolean, response?: azdata.CreateFirewallRuleResponse): TypeMoq.Mock<TestResourceProvider> {
let resourceProviderStub = new TestResourceProvider();
let mockResourceProvider = TypeMoq.Mock.ofInstance(resourceProviderStub);
mockResourceProvider.setup(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => resolveCreateFirewallRule ? Promise.resolve(response) : Promise.reject(null));
return mockResourceProvider;
}
function getMockErrorMessageService(deferredPromise: Deferred<{}>): TypeMoq.Mock<TestErrorMessageService> {
let errorMessageServiceStub = new TestErrorMessageService();
let mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback(() => {
deferredPromise.resolve(true);
});
return mockErrorMessageService;
}

View File

@@ -0,0 +1,176 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<form class="angular-form" #myForm="ngForm" (ngSubmit)="onSubmit(f)">
<div class="angular-modal-body" style="display: flex; flex-direction: column;">
<div class="angular-modal-body-content">
<div class="dialog-label">
{{localizedStrings.BACKUP_NAME}}
</div>
<div class="input-divider" #backupsetName>
</div>
<div class="dialog-label">
{{localizedStrings.RECOVERY_MODEL}}
</div>
<div class="input-divider" #recoveryModelContainer>
</div>
<div class="dialog-label">
{{localizedStrings.BACKUP_TYPE}}
</div>
<div class="input-divider" #backupTypeContainer>
</div>
<div class="input-divider check" #copyOnlyContainer>
</div>
<div class="dialog-label">
{{localizedStrings.BACKUP_DEVICE}}
</div>
<div class="backup-path-list">
<div #pathContainer>
</div>
</div>
<table class="backup-path-table">
<tr>
<td style="padding-left: 0px; padding-right: 0px;">
<div class="backup-path-button" #addPathContainer></div>
</td>
<td>
<div class="backup-path-button" #removePathContainer></div>
</td>
</tr>
</table>
<div class="advanced-main-header" #advancedOptionContainer>
<div class="advanced-main-body" #advancedOptionBodyContainer>
<!-- Compression -->
<div class="dialog-label advanced-header">
{{localizedStrings.COMPRESSION}}
</div>
<div class="indent">
<div class="dialog-label">
{{localizedStrings.SET_BACKUP_COMPRESSION}}
</div>
<div class="dialog-label" #compressionContainer>
</div>
</div>
<!-- Encryption -->
<div class="dialog-label advanced-header">
{{localizedStrings.ENCRYPTION}}
</div>
<div class="indent">
<div class="option check" #encryptCheckContainer>
</div>
<div class="option" #encryptWarningContainer>
<div class="sql icon warning">
</div>
<div class="warning-message">
{{localizedStrings.NO_ENCRYPTOR_WARNING}}
</div>
</div>
<div #encryptContainer>
<div class="dialog-label">
{{localizedStrings.ALGORITHM}}
</div>
<div class="dialog-label" #algorithmContainer>
</div>
<div class="dialog-label">
{{localizedStrings.CERTIFICATE_OR_ASYMMETRIC_KEY}}
</div>
<div class="dialog-label" #encryptorContainer>
</div>
</div>
</div>
<!-- Overwrite media -->
<div id="media" class="dialog-label advanced-header">
{{localizedStrings.MEDIA}}
</div>
<div role="radiogroup" aria-labelledby="media" class="radio-indent">
<div class="option">
<input role="radio" type="radio" name="media-option" value="no_format" [checked]="!isFormatChecked" (change)="onChangeMediaFormat()" [disabled]="isEncryptChecked" aria-labelledby="mediaOption"><span id="mediaOption">{{localizedStrings.MEDIA_OPTION}}</span>
</div>
<div role="radiogroup" aria-labelledby="mediaOption" style="margin-left:15px">
<div class="option">
<input role="radio" type="radio" name="existing-media" value="append" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked" aria-labelledby="existingMediaAppend"><span id="existingMediaAppend">{{localizedStrings.EXISTING_MEDIA_APPEND}}</span>
</div>
<div class="option">
<input role="radio" type="radio" name="existing-media" value="overwrite" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked" aria-labelledby="existingMediaOverwrite"><span id="existingMediaOverwrite">{{localizedStrings.EXISTING_MEDIA_OVERWRITE}}</span>
</div>
</div>
<div class="option">
<input role="radio" type="radio" name="media-option" value="format" [checked]="isFormatChecked" (change)="onChangeMediaFormat()" aria-labelledby="mediaOptionFormat"><span id="mediaOptionFormat">{{localizedStrings.MEDIA_OPTION_FORMAT}}</span>
</div>
<div style="margin-left: 22px">
<div class="dialog-label">
{{localizedStrings.NEW_MEDIA_SET_NAME}}
</div>
<div class="dialog-label" #mediaName>
</div>
<div class="dialog-label">
{{localizedStrings.NEW_MEDIA_SET_DESCRIPTION}}
</div>
<div class="dialog-label" #mediaDescription>
</div>
</div>
</div>
<!-- Transaction log -->
<div id="transactionLog" class="dialog-label advanced-header">
{{localizedStrings.TRANSACTION_LOG}}
</div>
<div role="radiogroup" aria-labelledby="transactionLog" class="radio-indent">
<div class="option">
<input role="radio" type="radio" name="t-log" value="truncate" [checked]="isTruncateChecked" (change)="onChangeTlog()" [disabled]="disableTlog" aria-labelledby="truncateTransaction"><span id="truncateTransaction">{{localizedStrings.TRUNCATE_TRANSACTION_LOG}}</span>
</div>
<div class="option">
<input role="radio" type="radio" name="t-log" value="taillog" [checked]="isTaillogChecked" (change)="onChangeTlog()" [disabled]="disableTlog" aria-labelledby="backupTail"><span id="backupTail">{{localizedStrings.BACKUP_TAIL}}</span>
</div>
</div>
<!-- Reliability -->
<div class="dialog-label advanced-header">
{{localizedStrings.RELIABILITY}}
</div>
<div class="indent">
<div class="option check" #checksumContainer>
</div>
<div class="option check" #verifyContainer>
</div>
<div class="option check" #continueOnErrorContainer>
</div>
</div>
<!-- Backup expiration -->
<div class="dialog-label advanced-header">
{{localizedStrings.EXPIRATION}}
</div>
<div class="indent">
<div class="dialog-label">
{{localizedStrings.SET_BACKUP_RETAIN_DAYS}}
</div>
<div class="dialog-label">
<div #backupDaysContainer></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer" #modalFooterContainer>
<div class="codicon in-progress" #inProgressContainer></div>
<div class="right-footer">
<div class="footer-button" #scriptButtonContainer>
</div>
<div class="footer-button" #backupButtonContainer>
</div>
<div class="footer-button" #cancelButtonContainer>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,914 @@
/*---------------------------------------------------------------------------------------------
* 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/backupDialog';
import { ElementRef, Component, Inject, forwardRef, ViewChild, ChangeDetectorRef } from '@angular/core';
import { Button } from 'sql/base/browser/ui/button/button';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { ListBox } from 'sql/base/browser/ui/listBox/listBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { attachButtonStyler, attachListBoxStyler, attachInputBoxStyler, attachSelectBoxStyler, attachCheckboxStyler } from 'sql/platform/theme/common/styler';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import * as BackupConstants from 'sql/workbench/contrib/backup/common/constants';
import { IBackupService, TaskExecutionMode } from 'sql/platform/backup/common/backupService';
import * as FileValidationConstants from 'sql/workbench/services/fileBrowser/common/fileValidationServiceConstants';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IFileBrowserDialogController } from 'sql/workbench/services/fileBrowser/common/fileBrowserDialogController';
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
import * as cr from 'vs/platform/theme/common/colorRegistry';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
import * as types from 'vs/base/common/types';
import * as strings from 'vs/base/common/strings';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox';
import { KeyCode } from 'vs/base/common/keyCodes';
import { ITheme } from 'vs/platform/theme/common/themeService';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
export const BACKUP_SELECTOR: string = 'backup-component';
export class RestoreItemSource {
restoreItemLocation: string;
restoreItemDeviceType: number;
isLogicalDevice: boolean;
constructor(location: any) {
this.restoreItemDeviceType = location.restoreItemDeviceType;
this.restoreItemLocation = location.restoreItemLocation;
this.isLogicalDevice = location.isLogicalDevice;
}
}
interface MssqlBackupInfo {
ownerUri: string;
databaseName: string;
backupType: number;
backupComponent: number;
backupDeviceType: number;
selectedFiles: string;
backupsetName: string;
selectedFileGroup: { [path: string]: string };
// List of {key: backup path, value: device type}
backupPathDevices: { [path: string]: number };
backupPathList: [string];
isCopyOnly: boolean;
formatMedia: boolean;
initialize: boolean;
skipTapeHeader: boolean;
mediaName: string;
mediaDescription: string;
checksum: boolean;
continueAfterError: boolean;
logTruncation: boolean;
tailLogBackup: boolean;
retainDays: number;
compressionOption: number;
verifyBackupRequired: boolean;
encryptionAlgorithm: number;
encryptorType: number;
encryptorName: string;
}
const LocalizedStrings = {
BACKUP_NAME: localize('backup.backupName', "Backup name"),
RECOVERY_MODEL: localize('backup.recoveryModel', "Recovery model"),
BACKUP_TYPE: localize('backup.backupType', "Backup type"),
BACKUP_DEVICE: localize('backup.backupDevice', "Backup files"),
ALGORITHM: localize('backup.algorithm', "Algorithm"),
CERTIFICATE_OR_ASYMMETRIC_KEY: localize('backup.certificateOrAsymmetricKey', "Certificate or Asymmetric key"),
MEDIA: localize('backup.media', "Media"),
MEDIA_OPTION: localize('backup.mediaOption', "Backup to the existing media set"),
MEDIA_OPTION_FORMAT: localize('backup.mediaOptionFormat', "Backup to a new media set"),
EXISTING_MEDIA_APPEND: localize('backup.existingMediaAppend', "Append to the existing backup set"),
EXISTING_MEDIA_OVERWRITE: localize('backup.existingMediaOverwrite', "Overwrite all existing backup sets"),
NEW_MEDIA_SET_NAME: localize('backup.newMediaSetName', "New media set name"),
NEW_MEDIA_SET_DESCRIPTION: localize('backup.newMediaSetDescription', "New media set description"),
CHECKSUM_CONTAINER: localize('backup.checksumContainer', "Perform checksum before writing to media"),
VERIFY_CONTAINER: localize('backup.verifyContainer', "Verify backup when finished"),
CONTINUE_ON_ERROR_CONTAINER: localize('backup.continueOnErrorContainer', "Continue on error"),
EXPIRATION: localize('backup.expiration', "Expiration"),
SET_BACKUP_RETAIN_DAYS: localize('backup.setBackupRetainDays', "Set backup retain days"),
COPY_ONLY: localize('backup.copyOnly', "Copy-only backup"),
ADVANCED_CONFIGURATION: localize('backup.advancedConfiguration', "Advanced Configuration"),
COMPRESSION: localize('backup.compression', "Compression"),
SET_BACKUP_COMPRESSION: localize('backup.setBackupCompression', "Set backup compression"),
ENCRYPTION: localize('backup.encryption', "Encryption"),
TRANSACTION_LOG: localize('backup.transactionLog', "Transaction log"),
TRUNCATE_TRANSACTION_LOG: localize('backup.truncateTransactionLog', "Truncate the transaction log"),
BACKUP_TAIL: localize('backup.backupTail', "Backup the tail of the log"),
RELIABILITY: localize('backup.reliability', "Reliability"),
MEDIA_NAME_REQUIRED_ERROR: localize('backup.mediaNameRequired', "Media name is required"),
NO_ENCRYPTOR_WARNING: localize('backup.noEncryptorWarning', "No certificate or asymmetric key is available")
};
@Component({
selector: BACKUP_SELECTOR,
templateUrl: decodeURI(require.toUrl('./backup.component.html'))
})
export class BackupComponent extends AngularDisposable {
@ViewChild('pathContainer', { read: ElementRef }) pathElement;
@ViewChild('backupTypeContainer', { read: ElementRef }) backupTypeElement;
@ViewChild('backupsetName', { read: ElementRef }) backupNameElement;
@ViewChild('compressionContainer', { read: ElementRef }) compressionElement;
@ViewChild('tlogOption', { read: ElementRef }) tlogOptionElement;
@ViewChild('algorithmContainer', { read: ElementRef }) encryptionAlgorithmElement;
@ViewChild('encryptorContainer', { read: ElementRef }) encryptorElement;
@ViewChild('mediaName', { read: ElementRef }) mediaNameElement;
@ViewChild('mediaDescription', { read: ElementRef }) mediaDescriptionElement;
@ViewChild('recoveryModelContainer', { read: ElementRef }) recoveryModelElement;
@ViewChild('backupDaysContainer', { read: ElementRef }) backupDaysElement;
@ViewChild('backupButtonContainer', { read: ElementRef }) backupButtonElement;
@ViewChild('cancelButtonContainer', { read: ElementRef }) cancelButtonElement;
@ViewChild('addPathContainer', { read: ElementRef }) addPathElement;
@ViewChild('removePathContainer', { read: ElementRef }) removePathElement;
@ViewChild('copyOnlyContainer', { read: ElementRef }) copyOnlyElement;
@ViewChild('encryptCheckContainer', { read: ElementRef }) encryptElement;
@ViewChild('encryptContainer', { read: ElementRef }) encryptContainerElement;
@ViewChild('verifyContainer', { read: ElementRef }) verifyElement;
@ViewChild('checksumContainer', { read: ElementRef }) checksumElement;
@ViewChild('continueOnErrorContainer', { read: ElementRef }) continueOnErrorElement;
@ViewChild('encryptWarningContainer', { read: ElementRef }) encryptWarningElement;
@ViewChild('inProgressContainer', { read: ElementRef }) inProgressElement;
@ViewChild('modalFooterContainer', { read: ElementRef }) modalFooterElement;
@ViewChild('scriptButtonContainer', { read: ElementRef }) scriptButtonElement;
@ViewChild('advancedOptionContainer', { read: ElementRef }) advancedOptionElement;
@ViewChild('advancedOptionBodyContainer', { read: ElementRef }) advancedOptionBodyElement;
private localizedStrings = LocalizedStrings;
private _uri: string;
private connection: IConnectionProfile;
private databaseName: string;
private defaultNewBackupFolder: string;
private recoveryModel: string;
private backupEncryptors;
private containsBackupToUrl: boolean;
// UI element disable flag
public disableTlog: boolean;
public selectedBackupComponent: string;
private selectedFilesText: string;
private selectedInitOption: string;
private isTruncateChecked: boolean;
private isTaillogChecked: boolean;
private isFormatChecked: boolean;
public isEncryptChecked: boolean;
// Key: backup path, Value: device type
private backupPathTypePairs: { [path: string]: number };
private compressionOptions = [BackupConstants.defaultCompression, BackupConstants.compressionOn, BackupConstants.compressionOff];
private encryptionAlgorithms = [BackupConstants.aes128, BackupConstants.aes192, BackupConstants.aes256, BackupConstants.tripleDES];
private existingMediaOptions = ['append', 'overwrite'];
private backupTypeOptions: string[];
private backupTypeSelectBox: SelectBox;
private backupNameBox: InputBox;
private recoveryBox: InputBox;
private backupRetainDaysBox: InputBox;
private backupButton: Button;
private cancelButton: Button;
private scriptButton: Button;
private addPathButton: Button;
private removePathButton: Button;
private pathListBox: ListBox;
private compressionSelectBox: SelectBox;
private algorithmSelectBox: SelectBox;
private encryptorSelectBox: SelectBox;
private mediaNameBox: InputBox;
private mediaDescriptionBox: InputBox;
private copyOnlyCheckBox: Checkbox;
private encryptCheckBox: Checkbox;
private verifyCheckBox: Checkbox;
private checksumCheckBox: Checkbox;
private continueOnErrorCheckBox: Checkbox;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeDetectorRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(IFileBrowserDialogController) private fileBrowserDialogService: IFileBrowserDialogController,
@Inject(IBackupUiService) private _backupUiService: IBackupUiService,
@Inject(IBackupService) private _backupService: IBackupService,
@Inject(IClipboardService) private clipboardService: IClipboardService,
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
) {
super();
this._backupUiService.onShowBackupEvent((param) => this.onGetBackupConfigInfo(param));
}
ngOnInit() {
let self = this;
this.addFooterButtons();
this.recoveryBox = new InputBox(this.recoveryModelElement.nativeElement, this.contextViewService, {
placeholder: this.recoveryModel,
ariaLabel: LocalizedStrings.RECOVERY_MODEL
});
// Set backup type
this.backupTypeSelectBox = new SelectBox([], '', this.contextViewService, undefined, { ariaLabel: this.localizedStrings.BACKUP_TYPE });
this.backupTypeSelectBox.render(this.backupTypeElement.nativeElement);
// Set copy-only check box
this.copyOnlyCheckBox = new Checkbox(this.copyOnlyElement.nativeElement, {
label: LocalizedStrings.COPY_ONLY,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.COPY_ONLY
});
// Encryption checkbox
this.encryptCheckBox = new Checkbox(this.encryptElement.nativeElement, {
label: LocalizedStrings.ENCRYPTION,
checked: false,
onChange: (viaKeyboard) => self.onChangeEncrypt(),
ariaLabel: LocalizedStrings.ENCRYPTION
});
// Verify backup checkbox
this.verifyCheckBox = new Checkbox(this.verifyElement.nativeElement, {
label: LocalizedStrings.VERIFY_CONTAINER,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.VERIFY_CONTAINER
});
// Perform checksum checkbox
this.checksumCheckBox = new Checkbox(this.checksumElement.nativeElement, {
label: LocalizedStrings.CHECKSUM_CONTAINER,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.CHECKSUM_CONTAINER
});
// Continue on error checkbox
this.continueOnErrorCheckBox = new Checkbox(this.continueOnErrorElement.nativeElement, {
label: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER
});
// Set backup name
this.backupNameBox = new InputBox(this.backupNameElement.nativeElement, this.contextViewService, {
ariaLabel: LocalizedStrings.BACKUP_NAME
});
// Set backup path list
this.pathListBox = new ListBox([], this.contextViewService);
this.pathListBox.setAriaLabel(LocalizedStrings.BACKUP_DEVICE);
this.pathListBox.onKeyDown(e => {
if (this.pathListBox.selectedOptions.length > 0) {
const key = e.keyCode;
const ctrlOrCmd = e.ctrlKey || e.metaKey;
if (ctrlOrCmd && key === KeyCode.KEY_C) {
let textToCopy = this.pathListBox.selectedOptions[0];
for (let i = 1; i < this.pathListBox.selectedOptions.length; i++) {
textToCopy = textToCopy + ', ' + this.pathListBox.selectedOptions[i];
}
// Copy to clipboard
this.clipboardService.writeText(textToCopy);
e.stopPropagation();
}
}
});
this.pathListBox.render(this.pathElement.nativeElement);
// Set backup path add/remove buttons
this.addPathButton = this._register(new Button(this.addPathElement.nativeElement));
this.addPathButton.label = '+';
this.addPathButton.title = localize('addFile', "Add a file");
this.removePathButton = this._register(new Button(this.removePathElement.nativeElement));
this.removePathButton.label = '-';
this.removePathButton.title = localize('removeFile', "Remove files");
// Set compression
this.compressionSelectBox = this._register(new SelectBox(this.compressionOptions, this.compressionOptions[0], this.contextViewService, undefined, { ariaLabel: this.localizedStrings.SET_BACKUP_COMPRESSION }));
this.compressionSelectBox.render(this.compressionElement.nativeElement);
// Set encryption
this.algorithmSelectBox = this._register(new SelectBox(this.encryptionAlgorithms, this.encryptionAlgorithms[0], this.contextViewService, undefined, { ariaLabel: this.localizedStrings.ALGORITHM }));
this.algorithmSelectBox.render(this.encryptionAlgorithmElement.nativeElement);
this.encryptorSelectBox = this._register(new SelectBox([], '', this.contextViewService, undefined, { ariaLabel: this.localizedStrings.CERTIFICATE_OR_ASYMMETRIC_KEY }));
this.encryptorSelectBox.render(this.encryptorElement.nativeElement);
// Set media
this.mediaNameBox = this._register(new InputBox(this.mediaNameElement.nativeElement,
this.contextViewService,
{
validationOptions: {
validation: (value: string) => !value ? ({ type: MessageType.ERROR, content: LocalizedStrings.MEDIA_NAME_REQUIRED_ERROR }) : null
},
ariaLabel: LocalizedStrings.NEW_MEDIA_SET_NAME
}
));
this.mediaDescriptionBox = this._register(new InputBox(this.mediaDescriptionElement.nativeElement, this.contextViewService, {
ariaLabel: LocalizedStrings.NEW_MEDIA_SET_DESCRIPTION
}));
// Set backup retain days
let invalidInputMessage = localize('backupComponent.invalidInput', "Invalid input. Value must be greater than or equal 0.");
this.backupRetainDaysBox = this._register(new InputBox(this.backupDaysElement.nativeElement,
this.contextViewService,
{
placeholder: '0',
type: 'number',
min: '0',
validationOptions: {
validation: (value: string) => {
if (types.isNumber(Number(value)) && Number(value) < 0) {
return { type: MessageType.ERROR, content: invalidInputMessage };
} else {
return null;
}
}
},
ariaLabel: LocalizedStrings.SET_BACKUP_RETAIN_DAYS
}));
// Disable elements
this.recoveryBox.disable();
this.mediaNameBox.disable();
this.mediaDescriptionBox.disable();
this.registerListeners();
this.updateTheme(this.themeService.getTheme());
}
ngAfterViewInit() {
this._backupUiService.onShowBackupDialog();
}
private onGetBackupConfigInfo(param: { connection: IConnectionProfile, ownerUri: string }) {
// Show spinner
this.showSpinner();
this.backupEnabled = false;
// Reset backup values
this.backupNameBox.value = '';
this.pathListBox.setOptions([], 0);
this.connection = param.connection;
this._uri = param.ownerUri;
// Get backup configuration info
this._backupService.getBackupConfigInfo(this._uri).then(configInfo => {
if (configInfo) {
this.defaultNewBackupFolder = configInfo.defaultBackupFolder;
this.recoveryModel = configInfo.recoveryModel;
this.backupEncryptors = configInfo.backupEncryptors;
this.initialize(true);
} else {
this.initialize(false);
}
// Hide spinner
this.hideSpinner();
});
}
/**
* Show spinner in the backup dialog
*/
private showSpinner(): void {
this.inProgressElement.nativeElement.style.visibility = 'visible';
}
/**
* Hide spinner in the backup dialog
*/
private hideSpinner(): void {
this.inProgressElement.nativeElement.style.visibility = 'hidden';
}
private addFooterButtons(): void {
// Set script footer button
this.scriptButton = this._register(new Button(this.scriptButtonElement.nativeElement));
this.scriptButton.label = localize('backupComponent.script', "Script");
this.addButtonClickHandler(this.scriptButton, () => this.onScript());
this._register(attachButtonStyler(this.scriptButton, this.themeService));
this.scriptButton.enabled = false;
// Set backup footer button
this.backupButton = this._register(new Button(this.backupButtonElement.nativeElement));
this.backupButton.label = localize('backupComponent.backup', "Backup");
this.addButtonClickHandler(this.backupButton, () => this.onOk());
this._register(attachButtonStyler(this.backupButton, this.themeService));
this.backupEnabled = false;
// Set cancel footer button
this.cancelButton = this._register(new Button(this.cancelButtonElement.nativeElement));
this.cancelButton.label = localize('backupComponent.cancel', "Cancel");
this.addButtonClickHandler(this.cancelButton, () => this.onCancel());
this._register(attachButtonStyler(this.cancelButton, this.themeService));
}
private initialize(isMetadataPopulated: boolean): void {
this.databaseName = this.connection.databaseName;
this.selectedBackupComponent = BackupConstants.labelDatabase;
this.backupPathTypePairs = {};
this.isFormatChecked = false;
this.isEncryptChecked = false;
this.selectedInitOption = this.existingMediaOptions[0];
this.backupTypeOptions = [];
if (isMetadataPopulated) {
this.backupEnabled = true;
// Set recovery model
this.setControlsForRecoveryModel();
// Set backup type
this.backupTypeSelectBox.setOptions(this.backupTypeOptions, 0);
this.setDefaultBackupName();
this.backupNameBox.focus();
// Set backup path list
this.setDefaultBackupPaths();
let pathlist: ISelectOptionItem[] = [];
for (let i in this.backupPathTypePairs) {
pathlist.push({ text: i });
}
this.pathListBox.setOptions(pathlist, 0);
// Set encryption
let encryptorItems = this.populateEncryptorCombo();
this.encryptorSelectBox.setOptions(encryptorItems, 0);
if (encryptorItems.length === 0) {
// Disable encryption checkbox
this.encryptCheckBox.disable();
// Show warning instead of algorithm select boxes
(<HTMLElement>this.encryptWarningElement.nativeElement).style.display = 'inline';
(<HTMLElement>this.encryptContainerElement.nativeElement).style.display = 'none';
}
else {
// Show algorithm select boxes instead of warning
(<HTMLElement>this.encryptWarningElement.nativeElement).style.display = 'none';
(<HTMLElement>this.encryptContainerElement.nativeElement).style.display = 'inline';
// Disable the algorithm select boxes since encryption is not checked by default
this.setEncryptOptionsEnabled(false);
}
this.setTLogOptions();
// disable elements
this.recoveryBox.disable();
this.mediaNameBox.disable();
this.mediaDescriptionBox.disable();
this.recoveryBox.value = this.recoveryModel;
// show warning message if latest backup file path contains url
if (this.containsBackupToUrl) {
this.pathListBox.setValidation(false, { content: localize('backup.containsBackupToUrlError', "Only backup to file is supported"), type: MessageType.WARNING });
this.pathListBox.focus();
}
}
this._changeDetectorRef.detectChanges();
}
/**
* Reset dialog controls to their initial state.
*/
private resetDialog(): void {
this.isFormatChecked = false;
this.isEncryptChecked = false;
this.copyOnlyCheckBox.checked = false;
this.copyOnlyCheckBox.enable();
this.compressionSelectBox.setOptions(this.compressionOptions, 0);
this.encryptCheckBox.checked = false;
this.encryptCheckBox.enable();
this.onChangeEncrypt();
this.mediaNameBox.value = '';
this.mediaDescriptionBox.value = '';
this.checksumCheckBox.checked = false;
this.verifyCheckBox.checked = false;
this.continueOnErrorCheckBox.checked = false;
this.backupRetainDaysBox.value = '0';
this.algorithmSelectBox.setOptions(this.encryptionAlgorithms, 0);
this.selectedInitOption = this.existingMediaOptions[0];
this.containsBackupToUrl = false;
this.pathListBox.setValidation(true);
this.cancelButton.applyStyles();
this.scriptButton.applyStyles();
this.backupButton.applyStyles();
}
private registerListeners(): void {
// Theme styler
this._register(attachInputBoxStyler(this.backupNameBox, this.themeService));
this._register(attachInputBoxStyler(this.recoveryBox, this.themeService));
this._register(attachSelectBoxStyler(this.backupTypeSelectBox, this.themeService));
this._register(attachListBoxStyler(this.pathListBox, this.themeService));
this._register(attachButtonStyler(this.addPathButton, this.themeService));
this._register(attachButtonStyler(this.removePathButton, this.themeService));
this._register(attachSelectBoxStyler(this.compressionSelectBox, this.themeService));
this._register(attachSelectBoxStyler(this.algorithmSelectBox, this.themeService));
this._register(attachSelectBoxStyler(this.encryptorSelectBox, this.themeService));
this._register(attachInputBoxStyler(this.mediaNameBox, this.themeService));
this._register(attachInputBoxStyler(this.mediaDescriptionBox, this.themeService));
this._register(attachInputBoxStyler(this.backupRetainDaysBox, this.themeService));
this._register(attachCheckboxStyler(this.copyOnlyCheckBox, this.themeService));
this._register(attachCheckboxStyler(this.encryptCheckBox, this.themeService));
this._register(attachCheckboxStyler(this.verifyCheckBox, this.themeService));
this._register(attachCheckboxStyler(this.checksumCheckBox, this.themeService));
this._register(attachCheckboxStyler(this.continueOnErrorCheckBox, this.themeService));
this._register(this.backupTypeSelectBox.onDidSelect(selected => this.onBackupTypeChanged()));
this.addButtonClickHandler(this.addPathButton, () => this.onAddClick());
this.addButtonClickHandler(this.removePathButton, () => this.onRemoveClick());
this._register(this.mediaNameBox.onDidChange(mediaName => {
this.mediaNameChanged(mediaName);
}));
this._register(this.backupRetainDaysBox.onDidChange(days => {
this.backupRetainDaysChanged(days);
}));
this._register(this.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
}
// Update theming that is specific to backup dialog
private updateTheme(theme: ITheme): void {
// set modal footer style
let footerHtmlElement: HTMLElement = <HTMLElement>this.modalFooterElement.nativeElement;
const backgroundColor = theme.getColor(SIDE_BAR_BACKGROUND);
const border = theme.getColor(cr.contrastBorder) ? theme.getColor(cr.contrastBorder).toString() : null;
const footerBorderTopWidth = border ? '1px' : null;
const footerBorderTopStyle = border ? 'solid' : null;
footerHtmlElement.style.backgroundColor = backgroundColor ? backgroundColor.toString() : null;
footerHtmlElement.style.borderTopWidth = footerBorderTopWidth;
footerHtmlElement.style.borderTopStyle = footerBorderTopStyle;
footerHtmlElement.style.borderTopColor = border;
}
private addButtonClickHandler(button: Button, handler: () => void) {
if (button && handler) {
button.onDidClick(() => {
if (button.enabled) {
handler();
}
});
}
}
/*
* UI event handlers
*/
private onScript(): void {
this._backupService.backup(this._uri, this.createBackupInfo(), TaskExecutionMode.script);
this.close();
}
private onOk(): void {
this._backupService.backup(this._uri, this.createBackupInfo(), TaskExecutionMode.executeAndScript);
this.close();
}
private onCancel(): void {
this.close();
this.connectionManagementService.disconnect(this._uri);
}
private close(): void {
this._backupUiService.closeBackup();
this.resetDialog();
}
public onChangeTlog(): void {
this.isTruncateChecked = !this.isTruncateChecked;
this.isTaillogChecked = !this.isTaillogChecked;
this.detectChange();
}
private onChangeEncrypt(): void {
if (this.encryptCheckBox.checked) {
this.setEncryptOptionsEnabled(true);
// Force to choose format media option since otherwise encryption cannot be done
if (!this.isFormatChecked) {
this.onChangeMediaFormat();
}
} else {
this.setEncryptOptionsEnabled(false);
}
this.isEncryptChecked = this.encryptCheckBox.checked;
this.detectChange();
}
private onChangeMediaFormat(): void {
this.isFormatChecked = !this.isFormatChecked;
this.enableMediaInput(this.isFormatChecked);
if (this.isFormatChecked) {
if (strings.isFalsyOrWhitespace(this.mediaNameBox.value)) {
this.backupEnabled = false;
this.backupButton.enabled = false;
this.mediaNameBox.showMessage({ type: MessageType.ERROR, content: LocalizedStrings.MEDIA_NAME_REQUIRED_ERROR });
}
} else {
this.enableBackupButton();
}
this.detectChange();
}
private set backupEnabled(value: boolean) {
this.backupButton.enabled = value;
this.scriptButton.enabled = value;
}
private onBackupTypeChanged(): void {
if (this.getSelectedBackupType() === BackupConstants.labelDifferential) {
this.copyOnlyCheckBox.checked = false;
this.copyOnlyCheckBox.disable();
} else {
this.copyOnlyCheckBox.enable();
}
this.setTLogOptions();
this.setDefaultBackupName();
this._changeDetectorRef.detectChanges();
}
private onAddClick(): void {
this.fileBrowserDialogService.showDialog(this._uri,
this.defaultNewBackupFolder,
BackupConstants.fileFiltersSet,
FileValidationConstants.backup,
false,
(filepath => this.handlePathAdded(filepath)));
}
private handlePathAdded(filepath: string) {
if (filepath && !this.backupPathTypePairs[filepath]) {
if ((this.getBackupPathCount() < BackupConstants.maxDevices)) {
this.backupPathTypePairs[filepath] = BackupConstants.deviceTypeFile;
this.pathListBox.add(filepath);
this.enableBackupButton();
this.enableAddRemoveButtons();
// stop showing error message if the list content was invalid due to no file path
if (!this.pathListBox.isContentValid && this.pathListBox.count === 1) {
this.pathListBox.setValidation(true);
}
this._changeDetectorRef.detectChanges();
}
}
}
private onRemoveClick(): void {
let self = this;
this.pathListBox.selectedOptions.forEach(selected => {
if (self.backupPathTypePairs[selected]) {
if (self.backupPathTypePairs[selected] === BackupConstants.deviceTypeURL) {
// stop showing warning message since url path is getting removed
this.pathListBox.setValidation(true);
this.containsBackupToUrl = false;
}
delete self.backupPathTypePairs[selected];
}
});
this.pathListBox.remove();
if (this.pathListBox.count === 0) {
this.backupEnabled = false;
// show input validation error
this.pathListBox.setValidation(false, { content: localize('backup.backupFileRequired', "Backup file path is required"), type: MessageType.ERROR });
this.pathListBox.focus();
}
this.enableAddRemoveButtons();
this._changeDetectorRef.detectChanges();
}
private enableAddRemoveButtons(): void {
if (this.pathListBox.count === 0) {
this.removePathButton.enabled = false;
} else if (this.pathListBox.count === BackupConstants.maxDevices) {
this.addPathButton.enabled = false;
} else {
this.removePathButton.enabled = true;
this.addPathButton.enabled = true;
}
}
/*
* Helper methods
*/
private setControlsForRecoveryModel(): void {
if (this.recoveryModel === BackupConstants.recoveryModelSimple) {
this.selectedBackupComponent = BackupConstants.labelDatabase;
}
this.populateBackupTypes();
}
private populateBackupTypes(): void {
this.backupTypeOptions.push(BackupConstants.labelFull);
if (this.databaseName !== 'master') {
this.backupTypeOptions.push(BackupConstants.labelDifferential);
if (this.recoveryModel !== BackupConstants.recoveryModelSimple) {
this.backupTypeOptions.push(BackupConstants.labelLog);
}
}
}
private populateEncryptorCombo(): string[] {
let encryptorCombo = [];
this.backupEncryptors.forEach((encryptor) => {
let encryptorTypeStr = (encryptor.encryptorType === 0 ? BackupConstants.serverCertificate : BackupConstants.asymmetricKey);
encryptorCombo.push(encryptor.encryptorName + '(' + encryptorTypeStr + ')');
});
return encryptorCombo;
}
private setDefaultBackupName(): void {
if (this.backupNameBox && (!this.backupNameBox.value || this.backupNameBox.value.trim().length === 0)) {
let utc = new Date().toJSON().slice(0, 19);
this.backupNameBox.value = this.databaseName + '-' + this.getSelectedBackupType() + '-' + utc;
}
}
private setDefaultBackupPaths(): void {
if (this.defaultNewBackupFolder && this.defaultNewBackupFolder.length > 0) {
// TEMPORARY WORKAROUND: karlb 5/27 - try to guess path separator on server based on first character in path
let serverPathSeparator: string = '\\';
if (this.defaultNewBackupFolder[0] === '/') {
serverPathSeparator = '/';
}
let d: Date = new Date();
let formattedDateTime: string = `-${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}-${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}`;
let defaultNewBackupLocation = this.defaultNewBackupFolder + serverPathSeparator + this.databaseName + formattedDateTime + '.bak';
// Add a default new backup location
this.backupPathTypePairs[defaultNewBackupLocation] = BackupConstants.deviceTypeFile;
}
}
private enableMediaInput(enable: boolean): void {
if (enable) {
this.mediaNameBox.enable();
this.mediaDescriptionBox.enable();
} else {
this.mediaNameBox.disable();
this.mediaDescriptionBox.disable();
}
}
private detectChange(): void {
this._changeDetectorRef.detectChanges();
}
private setTLogOptions(): void {
if (this.getSelectedBackupType() === BackupConstants.labelLog) {
// Enable log options
this.disableTlog = false;
// Choose the default option
this.isTruncateChecked = true;
} else {
// Unselect log options
this.isTruncateChecked = false;
this.isTaillogChecked = false;
// Disable log options
this.disableTlog = true;
}
}
private getBackupTypeNumber(): number {
let backupType;
switch (this.getSelectedBackupType()) {
case BackupConstants.labelFull:
backupType = 0;
break;
case BackupConstants.labelDifferential:
backupType = 1;
break;
case BackupConstants.labelLog:
backupType = 2;
break;
}
return backupType;
}
private getBackupPathCount(): number {
return this.pathListBox.count;
}
private getSelectedBackupType(): string {
let backupType = '';
if (this.backupTypeSelectBox) {
backupType = this.backupTypeSelectBox.value;
}
return backupType;
}
private enableBackupButton(): void {
if (!this.backupButton.enabled) {
if (this.pathListBox.count > 0 && (!this.isFormatChecked || this.mediaNameBox.value) && this.backupRetainDaysBox.validate()) {
this.backupEnabled = true;
}
}
}
private setEncryptOptionsEnabled(enabled: boolean): void {
if (enabled) {
this.algorithmSelectBox.enable();
this.encryptorSelectBox.enable();
} else {
this.algorithmSelectBox.disable();
this.encryptorSelectBox.disable();
}
}
private mediaNameChanged(mediaName: string): void {
if (!mediaName) {
this.backupEnabled = false;
} else {
this.enableBackupButton();
}
}
private backupRetainDaysChanged(days: string): void {
if (!this.backupRetainDaysBox.validate()) {
this.backupEnabled = false;
} else {
this.enableBackupButton();
}
}
private createBackupInfo(): MssqlBackupInfo {
let backupPathArray = [];
for (let i in this.backupPathTypePairs) {
backupPathArray.push(i);
}
// get encryptor type and name
let encryptorName = '';
let encryptorType;
if (this.encryptCheckBox.checked && this.encryptorSelectBox.value !== '') {
let selectedEncryptor = this.encryptorSelectBox.value;
let encryptorTypeStr = selectedEncryptor.substring(selectedEncryptor.lastIndexOf('(') + 1, selectedEncryptor.lastIndexOf(')'));
encryptorType = (encryptorTypeStr === BackupConstants.serverCertificate ? 0 : 1);
encryptorName = selectedEncryptor.substring(0, selectedEncryptor.lastIndexOf('('));
}
let backupInfo = <MssqlBackupInfo>{
ownerUri: this._uri,
databaseName: this.databaseName,
backupType: this.getBackupTypeNumber(),
backupComponent: 0,
backupDeviceType: BackupConstants.backupDeviceTypeDisk,
backupPathList: backupPathArray,
selectedFiles: this.selectedFilesText,
backupsetName: this.backupNameBox.value,
selectedFileGroup: undefined,
backupPathDevices: this.backupPathTypePairs,
isCopyOnly: this.copyOnlyCheckBox.checked,
// Get advanced options
formatMedia: this.isFormatChecked,
initialize: (this.isFormatChecked ? true : (this.selectedInitOption === this.existingMediaOptions[1])),
skipTapeHeader: this.isFormatChecked,
mediaName: (this.isFormatChecked ? this.mediaNameBox.value : ''),
mediaDescription: (this.isFormatChecked ? this.mediaDescriptionBox.value : ''),
checksum: this.checksumCheckBox.checked,
continueAfterError: this.continueOnErrorCheckBox.checked,
logTruncation: this.isTruncateChecked,
tailLogBackup: this.isTaillogChecked,
retainDays: strings.isFalsyOrWhitespace(this.backupRetainDaysBox.value) ? 0 : this.backupRetainDaysBox.value,
compressionOption: this.compressionOptions.indexOf(this.compressionSelectBox.value),
verifyBackupRequired: this.verifyCheckBox.checked,
encryptionAlgorithm: (this.encryptCheckBox.checked ? this.encryptionAlgorithms.indexOf(this.algorithmSelectBox.value) : 0),
encryptorType: encryptorType,
encryptorName: encryptorName
};
return backupInfo;
}
}

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { TreeViewItemHandleArg } from 'sql/workbench/common/views';
import { BackupAction } from 'sql/workbench/contrib/backup/browser/backupActions';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { ManageActionContext } from 'sql/workbench/browser/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext';
import { MssqlNodeContext } from 'sql/workbench/contrib/dataExplorer/browser/mssqlNodeContext';
import { NodeType } from 'sql/workbench/contrib/objectExplorer/common/nodeType';
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
import { localize } from 'vs/nls';
import { OEAction } from 'sql/workbench/contrib/objectExplorer/browser/objectExplorerActions';
import { TreeNodeContextKey } from 'sql/workbench/contrib/objectExplorer/common/treeNodeContextKey';
import { ConnectionContextKey } from 'sql/workbench/contrib/connection/common/connectionContextKey';
import { ServerInfoContextKey } from 'sql/workbench/contrib/connection/common/serverInfoContextKey';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes';
new BackupAction().registerTask();
// data explorer
const DE_BACKUP_COMMAND_ID = 'dataExplorer.backup';
CommandsRegistry.registerCommand({
id: DE_BACKUP_COMMAND_ID,
handler: (accessor, args: TreeViewItemHandleArg) => {
const commandService = accessor.get(ICommandService);
return commandService.executeCommand(BackupAction.ID, args.$treeItem.payload);
}
});
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
group: 'connection',
order: 4,
command: {
id: DE_BACKUP_COMMAND_ID,
title: localize('backup', "Backup")
},
when: ContextKeyExpr.and(MssqlNodeContext.NodeProvider.isEqualTo(mssqlProviderName),
MssqlNodeContext.NodeType.isEqualTo(NodeType.Database), MssqlNodeContext.IsCloud.toNegated(), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
});
// oe
const OE_BACKUP_COMMAND_ID = 'objectExplorer.backup';
CommandsRegistry.registerCommand({
id: OE_BACKUP_COMMAND_ID,
handler: (accessor: ServicesAccessor, actionContext: any) => {
const instantiationService = accessor.get(IInstantiationService);
return instantiationService.createInstance(OEAction, BackupAction.ID, BackupAction.LABEL).run(actionContext);
}
});
MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, {
group: 'connection',
order: 4,
command: {
id: OE_BACKUP_COMMAND_ID,
title: localize('backup', "Backup")
},
when: ContextKeyExpr.and(TreeNodeContextKey.NodeType.isEqualTo(NodeType.Database), ConnectionContextKey.Provider.isEqualTo(mssqlProviderName),
ServerInfoContextKey.IsCloud.toNegated(), ServerInfoContextKey.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
});
// dashboard explorer
const ExplorerBackUpActionID = 'explorer.backup';
CommandsRegistry.registerCommand(ExplorerBackUpActionID, (accessor, context: ManageActionContext) => {
const commandService = accessor.get(ICommandService);
return commandService.executeCommand(BackupAction.ID, context.profile);
});
MenuRegistry.appendMenuItem(MenuId.ExplorerWidgetContext, {
command: {
id: ExplorerBackUpActionID,
title: BackupAction.LABEL
},
when: ContextKeyExpr.and(ItemContextKey.ItemType.isEqualTo('database'), ItemContextKey.ConnectionProvider.isEqualTo('mssql'),
ItemContextKey.IsCloud.toNegated(), ItemContextKey.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString())),
order: 2
});

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
ApplicationRef, ComponentFactoryResolver, NgModule,
Inject, forwardRef, Type
} from '@angular/core';
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { providerIterator } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
import { BackupComponent } from 'sql/workbench/contrib/backup/browser/backup.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IBootstrapParams, ISelector } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
// Backup wizard main angular module
export const BackupModule = (params: IBootstrapParams, selector: string, instantiationService: IInstantiationService): Type<any> => {
@NgModule({
declarations: [
BackupComponent
],
entryComponents: [BackupComponent],
imports: [
FormsModule,
CommonModule,
BrowserModule
],
providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
{ provide: IBootstrapParams, useValue: params },
{ provide: ISelector, useValue: selector },
...providerIterator(instantiationService)
]
})
class ModuleClass {
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
@Inject(ISelector) private selector: string
) {
}
ngDoBootstrap(appRef: ApplicationRef) {
const factory = this._resolver.resolveComponentFactory(BackupComponent);
(<any>factory).factory.selector = this.selector;
appRef.bootstrap(factory);
}
}
return ModuleClass;
};

View File

@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { getCurrentGlobalConnection } from 'sql/workbench/browser/taskUtilities';
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
import { Task } from 'sql/platform/tasks/browser/tasksRegistry';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
export const BackupFeatureName = 'backup';
export function showBackup(accessor: ServicesAccessor, connection: IConnectionProfile): Promise<void> {
const backupUiService = accessor.get(IBackupUiService);
return backupUiService.showBackup(connection).then();
}
export class BackupAction extends Task {
public static readonly ID = BackupFeatureName;
public static readonly LABEL = localize('backupAction.backup', "Backup");
public static readonly ICON = BackupFeatureName;
constructor() {
super({
id: BackupAction.ID,
title: BackupAction.LABEL,
iconPath: undefined,
iconClass: BackupAction.ICON
});
}
runTask(accessor: ServicesAccessor, profile: IConnectionProfile): void | Promise<void> {
const configurationService = accessor.get<IConfigurationService>(IConfigurationService);
const previewFeaturesEnabled: boolean = configurationService.getValue('workbench')['enablePreviewFeatures'];
if (!previewFeaturesEnabled) {
return accessor.get<INotificationService>(INotificationService).info(localize('backup.isPreviewFeature', "You must enable preview features in order to use backup"));
}
const connectionManagementService = accessor.get<IConnectionManagementService>(IConnectionManagementService);
if (!profile) {
const objectExplorerService = accessor.get<IObjectExplorerService>(IObjectExplorerService);
const workbenchEditorService = accessor.get<IEditorService>(IEditorService);
profile = getCurrentGlobalConnection(objectExplorerService, connectionManagementService, workbenchEditorService);
}
if (profile) {
const serverInfo = connectionManagementService.getServerInfo(profile.id);
if (serverInfo && serverInfo.isCloud && profile.providerName === mssqlProviderName) {
return accessor.get<INotificationService>(INotificationService).info(localize('backup.commandNotSupported', "Backup command is not supported for Azure SQL databases."));
}
if (!profile.databaseName && profile.providerName === mssqlProviderName) {
return accessor.get<INotificationService>(INotificationService).info(localize('backup.commandNotSupportedForServer', "Backup command is not supported in Server Context. Please select a Database and try again."));
}
}
const capabilitiesService = accessor.get(ICapabilitiesService);
const instantiationService = accessor.get(IInstantiationService);
profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile);
return instantiationService.invokeFunction(showBackup, profile);
}
}

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Modal } from 'sql/workbench/browser/modal/modal';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { BackupModule } from 'sql/workbench/contrib/backup/browser/backup.module';
import { BACKUP_SELECTOR } from 'sql/workbench/contrib/backup/browser/backup.component';
import { attachModalDialogStyler } from 'sql/platform/theme/common/styler';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { bootstrapAngular } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { append, $ } from 'vs/base/browser/dom';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
export class BackupDialog extends Modal {
private _body: HTMLElement;
private _backupTitle: string;
private _moduleRef: any;
constructor(
@IThemeService themeService: IThemeService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
) {
super('', TelemetryKeys.Backup, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { isAngular: true, hasErrors: true });
}
protected renderBody(container: HTMLElement) {
this._body = append(container, $('.backup-dialog'));
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
// Add angular component template to dialog body
this.bootstrapAngular(this._body);
}
/**
* Get the bootstrap params and perform the bootstrap
*/
private bootstrapAngular(bodyContainer: HTMLElement) {
bootstrapAngular(this._instantiationService,
BackupModule,
bodyContainer,
BACKUP_SELECTOR,
undefined,
undefined,
(moduleRef) => this._moduleRef = moduleRef);
}
public hideError() {
this.showError('');
}
public showError(err: string) {
this.showError(err);
}
/* Overwrite escape key behavior */
protected onClose() {
this.close();
}
/**
* Clean up the module and DOM element and close the dialog
*/
public close() {
this.hide();
}
public dispose(): void {
super.dispose();
if (this._moduleRef) {
this._moduleRef.destroy();
}
}
/**
* Open the dialog
*/
public open(connection: IConnectionProfile) {
this._backupTitle = 'Backup database - ' + connection.serverName + ':' + connection.databaseName;
this.title = this._backupTitle;
this.show();
}
protected layout(height?: number): void {
// Nothing currently laid out in this class
}
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.backup-path-list {
overflow-x: auto;
}
.backup-path-table {
border: 0px;
border-collapse: collapse;
border-spacing: 0px;
}
.backup-path-button {
width: 22px;
}
.backup-dialog {
height: 100%
}
.backup-dialog .advanced-main-header {
padding-top: 20px;
}
.backup-dialog .advanced-header {
padding-top: 15px;
font-size: 14px;
}
.backup-dialog input[type="checkbox"] {
margin-left: 0px;
}
.backup-dialog input[type="radio"] {
margin-top: -2px;
vertical-align: middle;
}
.backup-dialog .indent {
margin-left: 7px;
}
.backup-dialog .radio-indent {
margin-left: 2px;
}
.backup-dialog .option {
width: 100%;
padding-bottom: 7px;
}
.backup-dialog .option.check {
display: flex;
padding-bottom: 4px;
}
.backup-dialog .check {
display: flex;
}
.backup-dialog .icon.warning {
width: 15px;
height: 15px;
float: left;
}
.backup-dialog .warning-message{
padding-left: 20px;
}

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
// Constants
export const maxDevices: number = 64;
// Constants for backup physical device type
export const backupDeviceTypeDisk = 2;
export const backupDeviceTypeTape = 5;
export const backupDeviceTypeURL = 9;
// Constants for backup media device type
export const deviceTypeLogicalDevice = 0;
export const deviceTypeTape = 1;
export const deviceTypeFile = 2;
export const deviceTypeURL = 5;
export const recoveryModelSimple = 'Simple';
export const recoveryModelFull = 'Full';
// Constants for UI strings
export const labelDatabase = localize('backup.labelDatabase', "Database");
export const labelFilegroup = localize('backup.labelFilegroup', "Files and filegroups");
export const labelFull = localize('backup.labelFull', "Full");
export const labelDifferential = localize('backup.labelDifferential', "Differential");
export const labelLog = localize('backup.labelLog', "Transaction Log");
export const labelDisk = localize('backup.labelDisk', "Disk");
export const labelUrl = localize('backup.labelUrl', "Url");
export const defaultCompression = localize('backup.defaultCompression', "Use the default server setting");
export const compressionOn = localize('backup.compressBackup', "Compress backup");
export const compressionOff = localize('backup.doNotCompress', "Do not compress backup");
export const aes128 = 'AES 128';
export const aes192 = 'AES 192';
export const aes256 = 'AES 256';
export const tripleDES = 'Triple DES';
export const serverCertificate = localize('backup.serverCertificate', "Server Certificate");
export const asymmetricKey = localize('backup.asymmetricKey', "Asymmetric Key");
export const fileFiltersSet: { label: string, filters: string[] }[] = [
{ label: localize('backup.filterBackupFiles', "Backup Files"), filters: ['*.bak', '*.trn', '*.log'] },
{ label: localize('backup.allFiles', "All Files"), filters: ['*'] }
];

View File

@@ -0,0 +1,196 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInsight } from 'sql/workbench/contrib/charts/browser/interfaces';
import { Graph } from 'sql/workbench/contrib/charts/browser/graphInsight';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { URI } from 'vs/base/common/uri';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { QueryInput } from 'sql/workbench/contrib/query/common/queryInput';
import { IInsightsConfig } from 'sql/platform/dashboard/browser/insightRegistry';
import { IInsightOptions } from 'sql/workbench/contrib/charts/common/interfaces';
import { IFileService } from 'vs/platform/files/common/files';
import { IFileDialogService, FileFilter } from 'vs/platform/dialogs/common/dialogs';
import { VSBuffer } from 'vs/base/common/buffer';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { assign } from 'vs/base/common/objects';
export interface IChartActionContext {
options: IInsightOptions;
insight: IInsight;
}
export class CreateInsightAction extends Action {
public static ID = 'chartview.createInsight';
public static LABEL = localize('createInsightLabel', "Create Insight");
public static ICON = 'createInsight';
constructor(
@IEditorService private editorService: IEditorService,
@INotificationService private notificationService: INotificationService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService
) {
super(CreateInsightAction.ID, CreateInsightAction.LABEL, CreateInsightAction.ICON);
}
public run(context: IChartActionContext): Promise<boolean> {
let uriString: string = this.getActiveUriString();
if (!uriString) {
this.showError(localize('createInsightNoEditor', "Cannot create insight as the active editor is not a SQL Editor"));
return Promise.resolve(false);
}
let uri: URI = URI.parse(uriString);
let queryFile: string = uri.fsPath;
let query: string = undefined;
let type = {};
let options = assign({}, context.options);
delete options.type;
type[context.options.type] = options;
// create JSON
let config: IInsightsConfig = {
type,
query,
queryFile
};
let widgetConfig = {
name: localize('myWidgetName', "My-Widget"),
gridItemConfig: {
sizex: 2,
sizey: 1
},
widget: {
'insights-widget': config
}
};
let input = this.untitledEditorService.createOrGet(undefined, 'json', JSON.stringify(widgetConfig));
return this.editorService.openEditor(input, { pinned: true })
.then(
() => true,
error => {
this.notificationService.notify({
severity: Severity.Error,
message: error
});
return false;
}
);
}
private getActiveUriString(): string {
let editor = this.editorService.activeEditor;
if (editor instanceof QueryInput) {
return editor.uri;
}
return undefined;
}
private showError(errorMsg: string) {
this.notificationService.notify({
severity: Severity.Error,
message: errorMsg
});
}
}
export class CopyAction extends Action {
public static ID = 'chartview.copy';
public static LABEL = localize('copyChartLabel', "Copy as image");
public static ICON = 'copyImage';
constructor(
@IClipboardService private clipboardService: IClipboardService,
@INotificationService private notificationService: INotificationService
) {
super(CopyAction.ID, CopyAction.LABEL, CopyAction.ICON);
}
public run(context: IChartActionContext): Promise<boolean> {
if (context.insight instanceof Graph) {
let data = context.insight.getCanvasData();
if (!data) {
this.showError(localize('chartNotFound', "Could not find chart to save"));
return Promise.resolve(false);
}
this.clipboardService.writeImageDataUrl(data);
return Promise.resolve(true);
}
return Promise.resolve(false);
}
private showError(errorMsg: string) {
this.notificationService.notify({
severity: Severity.Error,
message: errorMsg
});
}
}
export class SaveImageAction extends Action {
public static ID = 'chartview.saveImage';
public static LABEL = localize('saveImageLabel', "Save as image");
public static ICON = 'saveAsImage';
constructor(
@INotificationService private readonly notificationService: INotificationService,
@IFileService private readonly fileService: IFileService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IOpenerService private readonly openerService: IOpenerService
) {
super(SaveImageAction.ID, SaveImageAction.LABEL, SaveImageAction.ICON);
}
public async run(context: IChartActionContext): Promise<boolean> {
if (context.insight instanceof Graph) {
let fileFilters = new Array<FileFilter>({ extensions: ['png'], name: localize('resultsSerializer.saveAsFileExtensionPNGTitle', "PNG") });
const filePath = await this.fileDialogService.pickFileToSave({ filters: fileFilters });
const data = (<Graph>context.insight).getCanvasData();
if (!data) {
this.notificationService.error(localize('chartNotFound', "Could not find chart to save"));
return false;
}
if (filePath) {
let buffer = this.decodeBase64Image(data);
try {
await this.fileService.writeFile(filePath, buffer);
} catch (err) {
if (err) {
this.notificationService.error(err.message);
} else {
this.openerService.open(filePath, { openExternal: true });
this.notificationService.notify({
severity: Severity.Error,
message: localize('chartSaved', "Saved Chart to path: {0}", filePath.toString())
});
}
}
}
return true;
}
return Promise.resolve(false);
}
private decodeBase64Image(data: string): VSBuffer {
const marker = ';base64,';
const raw = atob(data.substring(data.indexOf(marker) + marker.length));
const n = raw.length;
const a = new Uint8Array(new ArrayBuffer(n));
for (let i = 0; i < n; i++) {
a[i] = raw.charCodeAt(i);
}
return VSBuffer.wrap(a);
}
}

View File

@@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/browser/insightRegistry';
import { IInsightOptions, DataDirection, DataType, LegendPosition, ChartType, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
import { values } from 'vs/base/common/collections';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
export enum ControlType {
combo,
numberInput,
input,
checkbox,
dateInput
}
export interface IChartOption {
label: string;
type: ControlType;
configEntry: string;
default: any;
options?: any[];
displayableOptions?: string[];
if?: (options: IInsightOptions) => boolean;
}
export interface IChartOptions {
general: Array<IChartOption>;
[x: string]: Array<IChartOption>;
}
const dataDirectionOption: IChartOption = {
label: localize('dataDirectionLabel', "Data Direction"),
type: ControlType.combo,
displayableOptions: [localize('verticalLabel', "Vertical"), localize('horizontalLabel', "Horizontal")],
options: [DataDirection.Vertical, DataDirection.Horizontal],
configEntry: 'dataDirection',
default: DataDirection.Horizontal
};
const columnsAsLabelsInput: IChartOption = {
label: localize('columnsAsLabelsLabel', "Use column names as labels"),
type: ControlType.checkbox,
configEntry: 'columnsAsLabels',
default: true,
if: (options: IInsightOptions) => {
return options.dataDirection === DataDirection.Vertical && options.dataType !== DataType.Point;
}
};
const labelFirstColumnInput: IChartOption = {
label: localize('labelFirstColumnLabel', "Use first column as row label"),
type: ControlType.checkbox,
configEntry: 'labelFirstColumn',
default: false,
if: (options: IInsightOptions) => {
return options.dataDirection === DataDirection.Horizontal && options.dataType !== DataType.Point;
}
};
const legendInput: IChartOption = {
label: localize('legendLabel', "Legend Position"),
type: ControlType.combo,
options: values(LegendPosition),
configEntry: 'legendPosition',
default: LegendPosition.Top
};
const yAxisLabelInput: IChartOption = {
label: localize('yAxisLabel', "Y Axis Label"),
type: ControlType.input,
configEntry: 'yAxisLabel',
default: undefined
};
const yAxisMinInput: IChartOption = {
label: localize('yAxisMinVal', "Y Axis Minimum Value"),
type: ControlType.numberInput,
configEntry: 'yAxisMin',
default: undefined
};
const yAxisMaxInput: IChartOption = {
label: localize('yAxisMaxVal', "Y Axis Maximum Value"),
type: ControlType.numberInput,
configEntry: 'yAxisMax',
default: undefined
};
const xAxisLabelInput: IChartOption = {
label: localize('xAxisLabel', "X Axis Label"),
type: ControlType.input,
configEntry: 'xAxisLabel',
default: undefined
};
const xAxisMinInput: IChartOption = {
label: localize('xAxisMinVal', "X Axis Minimum Value"),
type: ControlType.numberInput,
configEntry: 'xAxisMin',
default: undefined
};
const xAxisMaxInput: IChartOption = {
label: localize('xAxisMaxVal', "X Axis Maximum Value"),
type: ControlType.numberInput,
configEntry: 'xAxisMax',
default: undefined
};
const xAxisMinDateInput: IChartOption = {
label: localize('xAxisMinDate', "X Axis Minimum Date"),
type: ControlType.dateInput,
configEntry: 'xAxisMin',
default: undefined
};
const xAxisMaxDateInput: IChartOption = {
label: localize('xAxisMaxDate', "X Axis Maximum Date"),
type: ControlType.dateInput,
configEntry: 'xAxisMax',
default: undefined
};
const dataTypeInput: IChartOption = {
label: localize('dataTypeLabel', "Data Type"),
type: ControlType.combo,
options: [DataType.Number, DataType.Point],
displayableOptions: [localize('numberLabel', "Number"), localize('pointLabel', "Point")],
configEntry: 'dataType',
default: DataType.Number
};
export const ChartOptions: IChartOptions = {
general: [
{
label: localize('chartTypeLabel', "Chart Type"),
type: ControlType.combo,
options: insightRegistry.getAllIds(),
configEntry: 'type',
default: ChartType.Bar
}
],
[ChartType.Line]: [
dataTypeInput,
columnsAsLabelsInput,
labelFirstColumnInput,
yAxisLabelInput,
xAxisLabelInput,
legendInput
],
[ChartType.Scatter]: [
legendInput,
yAxisLabelInput,
xAxisLabelInput
],
[ChartType.TimeSeries]: [
legendInput,
yAxisLabelInput,
yAxisMinInput,
yAxisMaxInput,
xAxisLabelInput,
xAxisMinDateInput,
xAxisMaxDateInput,
],
[ChartType.Bar]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput,
yAxisLabelInput,
yAxisMinInput,
yAxisMaxInput,
xAxisLabelInput
],
[ChartType.HorizontalBar]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput,
xAxisLabelInput,
xAxisMinInput,
xAxisMaxInput,
yAxisLabelInput
],
[ChartType.Pie]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput
],
[ChartType.Doughnut]: [
dataDirectionOption,
columnsAsLabelsInput,
labelFirstColumnInput,
legendInput
],
[InsightType.Table]: [],
[InsightType.Count]: [],
[InsightType.Image]: [
{
configEntry: 'encoding',
label: localize('encodingOption', "Encoding"),
type: ControlType.input,
default: 'hex'
},
{
configEntry: 'imageFormat',
label: localize('imageFormatOption', "Image Format"),
type: ControlType.input,
default: 'jpeg'
}
]
};

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPanelTab } from 'sql/base/browser/ui/panel/panel';
import { ChartView } from './chartView';
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class ChartTab implements IPanelTab {
public readonly title = localize('chartTabTitle', "Chart");
public readonly identifier = 'ChartTab';
public readonly view: ChartView;
constructor(@IInstantiationService instantiationService: IInstantiationService) {
this.view = instantiationService.createInstance(ChartView);
}
public set queryRunner(runner: QueryRunner) {
this.view.queryRunner = runner;
}
public chart(dataId: { batchId: number, resultId: number }): void {
this.view.chart(dataId);
}
public dispose() {
this.view.dispose();
}
public clear() {
this.view.clear();
}
}

View File

@@ -0,0 +1,399 @@
/*---------------------------------------------------------------------------------------------
* 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/chartView';
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { Insight } from './insight';
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { ChartOptions, IChartOption, ControlType } from './chartOptions';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/browser/insightRegistry';
import { IInsightData } from './interfaces';
import { Registry } from 'vs/platform/registry/common/platform';
import * as DOM from 'vs/base/browser/dom';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { attachSelectBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { CreateInsightAction, CopyAction, SaveImageAction, IChartActionContext } from 'sql/workbench/contrib/charts/browser/actions';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { ChartState, IInsightOptions, ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
import * as nls from 'vs/nls';
import { find } from 'vs/base/common/arrays';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
//Map used to store names and alternative names for chart types.
//This is mainly used for comparison when options are parsed into the constructor.
const altNameHash: { [oldName: string]: string } = {
'horizontalBar': nls.localize('horizontalBarAltName', "Horizontal Bar"),
'bar': nls.localize('barAltName', "Bar"),
'line': nls.localize('lineAltName', "Line"),
'pie': nls.localize('pieAltName', "Pie"),
'scatter': nls.localize('scatterAltName', "Scatter"),
'timeSeries': nls.localize('timeSeriesAltName', "Time Series"),
'image': nls.localize('imageAltName', "Image"),
'count': nls.localize('countAltName', "Count"),
'table': nls.localize('tableAltName', "Table"),
'doughnut': nls.localize('doughnutAltName', "Doughnut")
};
export class ChartView extends Disposable implements IPanelView {
private insight: Insight;
private _queryRunner: QueryRunner;
private _data: IInsightData;
private _currentData: { batchId: number, resultId: number };
private taskbar: Taskbar;
private _createInsightAction: CreateInsightAction;
private _copyAction: CopyAction;
private _saveAction: SaveImageAction;
private _state: ChartState;
private options: IInsightOptions = {
type: ChartType.Bar
};
/** parent container */
private container: HTMLElement;
/** container for the options controls */
private optionsControl: HTMLElement;
/** container for type specific controls */
private typeControls: HTMLElement;
/** container for the insight */
private insightContainer: HTMLElement;
/** container for the action bar */
private taskbarContainer: HTMLElement;
/** container for the charting (includes insight and options) */
private chartingContainer: HTMLElement;
private optionDisposables: IDisposable[] = [];
private optionMap: { [x: string]: { element: HTMLElement; set: (val) => void } } = {};
constructor(
@IContextViewService private _contextViewService: IContextViewService,
@IThemeService private _themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
) {
super();
this.taskbarContainer = DOM.$('div.taskbar-container');
this.taskbar = new Taskbar(this.taskbarContainer);
this.optionsControl = DOM.$('div.options-container');
const generalControls = DOM.$('div.general-controls');
this.optionsControl.appendChild(generalControls);
this.typeControls = DOM.$('div.type-controls');
this.optionsControl.appendChild(this.typeControls);
this._createInsightAction = this._instantiationService.createInstance(CreateInsightAction);
this._copyAction = this._instantiationService.createInstance(CopyAction);
this._saveAction = this._instantiationService.createInstance(SaveImageAction);
this.taskbar.setContent([{ action: this._createInsightAction }]);
const self = this;
this.options = new Proxy(this.options, {
get: function (target, key) {
return target[key];
},
set: function (target, key, value) {
let change = false;
if (target[key] !== value) {
change = true;
}
target[key] = value;
// mirror the change in our state
if (self.state) {
self.state.options[key] = value;
}
if (change) {
self.taskbar.context = <IChartActionContext>{ options: self.options, insight: self.insight ? self.insight.insight : undefined };
if (key === 'type') {
self.buildOptions();
} else {
self.verifyOptions();
}
}
return true;
}
}) as IInsightOptions;
ChartOptions.general[0].options = insightRegistry.getAllIds();
ChartOptions.general.map(o => {
this.createOption(o, generalControls);
});
this.buildOptions();
}
public clear() {
}
/**
* Function used to generate list of alternative names for use with SelectBox
* @param option - the original option names.
*/
private changeToAltNames(option: string[]): string[] {
return option.map(o => altNameHash[o] || o);
}
public dispose() {
dispose(this.optionDisposables);
super.dispose();
}
render(container: HTMLElement): void {
if (!this.container) {
this.container = DOM.$('div.chart-parent-container');
this.insightContainer = DOM.$('div.insight-container');
this.chartingContainer = DOM.$('div.charting-container');
this.container.appendChild(this.taskbarContainer);
this.container.appendChild(this.chartingContainer);
this.chartingContainer.appendChild(this.insightContainer);
this.chartingContainer.appendChild(this.optionsControl);
this.insight = new Insight(this.insightContainer, this.options, this._instantiationService);
}
container.appendChild(this.container);
if (this._data) {
this.insight.data = this._data;
} else {
this.queryRunner = this._queryRunner;
}
this.verifyOptions();
}
public chart(dataId: { batchId: number, resultId: number }) {
this.state.dataId = dataId;
this._currentData = dataId;
this.shouldGraph();
}
layout(dimension: DOM.Dimension): void {
if (this.insight) {
this.insight.layout(new DOM.Dimension(DOM.getContentWidth(this.insightContainer), DOM.getContentHeight(this.insightContainer)));
}
}
focus(): void {
}
public set queryRunner(runner: QueryRunner) {
this._queryRunner = runner;
this.shouldGraph();
}
private shouldGraph() {
// Check if we have the necessary information
if (this._currentData && this._queryRunner) {
// check if we are being asked to graph something that is available
let batch = this._queryRunner.batchSets[this._currentData.batchId];
if (batch) {
let summary = batch.resultSetSummaries[this._currentData.resultId];
if (summary) {
this._queryRunner.getQueryRows(0, summary.rowCount, 0, 0).then(d => {
this._data = {
columns: summary.columnInfo.map(c => c.columnName),
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
};
if (this.insight) {
this.insight.data = this._data;
}
});
}
}
// if we have the necessary information but the information isn't available yet,
// we should be smart and retrying when the information might be available
}
}
private buildOptions() {
// The first element in the disposables list is for the chart type: the master dropdown that controls other option controls.
// whiling rebuilding the options we should not dispose it, otherwise it would react to the theme change event
if (this.optionDisposables.length > 1) { // this logic needs to be rewritten
dispose(this.optionDisposables.slice(1));
this.optionDisposables.splice(1);
}
this.optionMap = {
'type': this.optionMap['type']
};
DOM.clearNode(this.typeControls);
this.updateActionbar();
ChartOptions[this.options.type].map(o => {
this.createOption(o, this.typeControls);
});
if (this.insight) {
this.insight.options = this.options;
}
this.verifyOptions();
}
private verifyOptions() {
this.updateActionbar();
for (let key in this.optionMap) {
if (this.optionMap.hasOwnProperty(key)) {
let option = find(ChartOptions[this.options.type], e => e.configEntry === key);
if (option && option.if) {
if (option.if(this.options)) {
DOM.show(this.optionMap[key].element);
} else {
DOM.hide(this.optionMap[key].element);
}
}
}
}
}
private updateActionbar() {
if (this.insight && this.insight.isCopyable) {
this.taskbar.context = { insight: this.insight.insight, options: this.options };
this.taskbar.setContent([
{ action: this._createInsightAction },
{ action: this._copyAction },
{ action: this._saveAction }
]);
} else {
this.taskbar.setContent([{ action: this._createInsightAction }]);
}
}
private createOption(option: IChartOption, container: HTMLElement) {
let label = DOM.$('div');
label.innerText = option.label;
let optionContainer = DOM.$('div.option-container');
optionContainer.appendChild(label);
let setFunc: (val) => void;
let value = this.state ? this.state.options[option.configEntry] || option.default : option.default;
switch (option.type) {
case ControlType.checkbox:
let checkbox = new Checkbox(optionContainer, {
label: '',
ariaLabel: option.label,
checked: value,
onChange: () => {
if (this.options[option.configEntry] !== checkbox.checked) {
this.options[option.configEntry] = checkbox.checked;
if (this.insight) {
this.insight.options = this.options;
}
}
}
});
setFunc = (val: boolean) => {
checkbox.checked = val;
};
break;
case ControlType.combo:
//pass options into changeAltNames in order for SelectBox to show user-friendly names.
let dropdown = new SelectBox(option.displayableOptions || this.changeToAltNames(option.options), undefined, this._contextViewService);
dropdown.select(option.options.indexOf(value));
dropdown.render(optionContainer);
dropdown.onDidSelect(e => {
if (this.options[option.configEntry] !== option.options[e.index]) {
this.options[option.configEntry] = option.options[e.index];
if (this.insight) {
this.insight.options = this.options;
}
}
});
setFunc = (val: string) => {
if (!isUndefinedOrNull(val)) {
dropdown.select(option.options.indexOf(val));
}
};
this.optionDisposables.push(attachSelectBoxStyler(dropdown, this._themeService));
break;
case ControlType.input:
let input = new InputBox(optionContainer, this._contextViewService);
input.value = value || '';
input.onDidChange(e => {
if (this.options[option.configEntry] !== e) {
this.options[option.configEntry] = e;
if (this.insight) {
this.insight.options = this.options;
}
}
});
setFunc = (val: string) => {
if (!isUndefinedOrNull(val)) {
input.value = val;
}
};
this.optionDisposables.push(attachInputBoxStyler(input, this._themeService));
break;
case ControlType.numberInput:
let numberInput = new InputBox(optionContainer, this._contextViewService, { type: 'number' });
numberInput.value = value || '';
numberInput.onDidChange(e => {
if (this.options[option.configEntry] !== Number(e)) {
this.options[option.configEntry] = Number(e);
if (this.insight) {
this.insight.options = this.options;
}
}
});
setFunc = (val: string) => {
if (!isUndefinedOrNull(val)) {
numberInput.value = val;
}
};
this.optionDisposables.push(attachInputBoxStyler(numberInput, this._themeService));
break;
case ControlType.dateInput:
let dateInput = new InputBox(optionContainer, this._contextViewService, { type: 'datetime-local' });
dateInput.value = value || '';
dateInput.onDidChange(e => {
if (this.options[option.configEntry] !== e) {
this.options[option.configEntry] = e;
if (this.insight) {
this.insight.options = this.options;
}
}
});
setFunc = (val: string) => {
if (!isUndefinedOrNull(val)) {
dateInput.value = val;
}
};
this.optionDisposables.push(attachInputBoxStyler(dateInput, this._themeService));
break;
}
this.optionMap[option.configEntry] = { element: optionContainer, set: setFunc };
container.appendChild(optionContainer);
this.options[option.configEntry] = value;
}
public set state(val: ChartState) {
this._state = val;
if (this.state.options) {
for (let key in this.state.options) {
if (this.state.options.hasOwnProperty(key) && this.optionMap[key]) {
this.options[key] = this.state.options[key];
this.optionMap[key].set(this.state.options[key]);
}
}
}
if (this.state.dataId) {
this.chart(this.state.dataId);
}
}
public get state(): ChartState {
return this._state;
}
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* 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/countInsight';
import { IInsight, IInsightData } from './interfaces';
import { $, clearNode } from 'vs/base/browser/dom';
import { InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
export class CountInsight implements IInsight {
public options;
public static readonly types = [InsightType.Count];
public readonly types = CountInsight.types;
private countImage: HTMLElement;
constructor(container: HTMLElement, options: any) {
this.countImage = $('div');
container.appendChild(this.countImage);
}
public layout() { }
set data(data: IInsightData) {
clearNode(this.countImage);
for (let i = 0; i < data.columns.length; i++) {
let container = $('div.count-label-container');
let label = $('span.label-container');
label.innerText = data.columns[i];
let value = $('span.value-container');
value.innerText = data.rows[0][i];
container.appendChild(label);
container.appendChild(value);
this.countImage.appendChild(container);
}
}
dispose() {
}
}

View File

@@ -0,0 +1,441 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as chartjs from 'chart.js';
import { mixin } from 'sql/base/common/objects';
import { localize } from 'vs/nls';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { IInsight, IInsightData, IPointDataSet, customMixin } from './interfaces';
import { IInsightOptions, DataDirection, ChartType, LegendPosition, DataType } from 'sql/workbench/contrib/charts/common/interfaces';
import { values } from 'vs/base/common/collections';
import { find } from 'vs/base/common/arrays';
const noneLineGraphs = [ChartType.Doughnut, ChartType.Pie];
const timeSeriesScales: chartjs.ChartOptions = {
scales: {
xAxes: [{
type: 'time',
display: true,
ticks: {
autoSkip: false,
maxRotation: 45,
minRotation: 45
}
}],
yAxes: [{
display: true,
}]
}
};
const defaultOptions: IInsightOptions = {
type: ChartType.Bar,
dataDirection: DataDirection.Horizontal
};
export class Graph implements IInsight {
private _options: IInsightOptions;
private canvas: HTMLCanvasElement;
private chartjs: chartjs;
private _data: IInsightData;
private originalType: ChartType;
public static readonly types = [ChartType.Bar, ChartType.Doughnut, ChartType.HorizontalBar, ChartType.Line, ChartType.Pie, ChartType.Scatter, ChartType.TimeSeries];
public readonly types = Graph.types;
private _theme: ITheme;
constructor(
container: HTMLElement, options: IInsightOptions = defaultOptions,
@IThemeService themeService: IThemeService
) {
this._theme = themeService.getTheme();
themeService.onThemeChange(e => {
this._theme = e;
this.data = this._data;
});
this.options = mixin(options, defaultOptions, false);
let canvasContainer = document.createElement('div');
canvasContainer.style.width = '100%';
canvasContainer.style.height = '100%';
this.canvas = document.createElement('canvas');
canvasContainer.appendChild(this.canvas);
container.appendChild(canvasContainer);
}
public dispose() {
}
public layout() {
}
public getCanvasData(): string {
return this.chartjs.toBase64Image();
}
public set data(data: IInsightData) {
if (!data) {
return;
}
this._data = data;
let labels: Array<string>;
let chartData: Array<Chart.ChartDataSets>;
if (this.options.dataDirection === DataDirection.Horizontal) {
if (this.options.labelFirstColumn) {
labels = data.columns.slice(1);
} else {
labels = data.columns;
}
} else {
labels = data.rows.map(row => row[0]);
}
if (this.originalType === ChartType.TimeSeries) {
let dataSetMap: { [label: string]: IPointDataSet } = {};
this._data.rows.map(row => {
if (row && row.length >= 3) {
let legend = row[0];
if (!dataSetMap[legend]) {
dataSetMap[legend] = { label: legend, data: [], fill: false };
}
dataSetMap[legend].data.push({ x: row[1], y: Number(row[2]) });
}
});
chartData = values(dataSetMap);
} else {
if (this.options.dataDirection === DataDirection.Horizontal) {
if (this.options.labelFirstColumn) {
chartData = data.rows.map((row) => {
return {
data: row.map(item => Number(item)).slice(1),
label: row[0]
};
});
} else {
chartData = data.rows.map((row, i) => {
return {
data: row.map(item => Number(item)),
label: localize('series', "Series {0}", i)
};
});
}
} else {
if (this.options.columnsAsLabels) {
chartData = data.rows[0].slice(1).map((row, i) => {
return {
data: data.rows.map(row => Number(row[i + 1])),
label: data.columns[i + 1]
};
});
} else {
chartData = data.rows[0].slice(1).map((row, i) => {
return {
data: data.rows.map(row => Number(row[i + 1])),
label: localize('series', "Series {0}", i + 1)
};
});
}
}
}
chartData = chartData.map((c, i) => {
return mixin(c, getColors(this.options.type, i, c.data.length), false);
});
if (this.chartjs) {
this.chartjs.data.datasets = chartData;
this.chartjs.config.type = this.options.type;
// we don't want to include lables for timeSeries
this.chartjs.data.labels = this.originalType === 'timeSeries' ? [] : labels;
this.chartjs.config.options = this.transformOptions(this.options);
this.chartjs.update(0);
} else {
this.chartjs = new chartjs.Chart(this.canvas.getContext('2d'), {
data: {
// we don't want to include lables for timeSeries
labels: this.originalType === 'timeSeries' ? [] : labels,
datasets: chartData
},
type: this.options.type,
options: this.transformOptions(this.options)
});
}
}
private transformOptions(options: IInsightOptions): Chart.ChartOptions {
let retval: Chart.ChartOptions = {};
retval.maintainAspectRatio = false;
let foregroundColor = this._theme.getColor(colors.editorForeground);
let foreground = foregroundColor ? foregroundColor.toString() : null;
let gridLinesColor = this._theme.getColor(editorLineNumbers);
let gridLines = gridLinesColor ? gridLinesColor.toString() : null;
let backgroundColor = this._theme.getColor(colors.editorBackground);
let background = backgroundColor ? backgroundColor.toString() : null;
if (options) {
retval.scales = {};
// we only want to include axis if it is a axis based graph type
if (!find(noneLineGraphs, x => x === options.type as ChartType)) {
retval.scales.xAxes = [{
scaleLabel: {
fontColor: foreground,
labelString: options.xAxisLabel,
display: options.xAxisLabel ? true : false
},
ticks: {
fontColor: foreground
},
gridLines: {
color: gridLines
}
}];
if (options.xAxisMax) {
retval.scales = mixin(retval.scales, { xAxes: [{ ticks: { max: options.xAxisMax } }] }, true, customMixin);
}
if (options.xAxisMin) {
retval.scales = mixin(retval.scales, { xAxes: [{ ticks: { min: options.xAxisMin } }] }, true, customMixin);
}
retval.scales.yAxes = [{
scaleLabel: {
fontColor: foreground,
labelString: options.yAxisLabel,
display: options.yAxisLabel ? true : false
},
ticks: {
fontColor: foreground
},
gridLines: {
color: gridLines
}
}];
if (options.yAxisMax) {
retval.scales = mixin(retval.scales, { yAxes: [{ ticks: { max: options.yAxisMax } }] }, true, customMixin);
}
if (options.yAxisMin) {
retval.scales = mixin(retval.scales, { yAxes: [{ ticks: { min: options.yAxisMin } }] }, true, customMixin);
}
if (this.originalType === ChartType.TimeSeries) {
retval = mixin(retval, timeSeriesScales, true, customMixin);
if (options.xAxisMax) {
retval = mixin(retval, {
scales: {
xAxes: [{
time: {
max: options.xAxisMax
}
}],
}
}, true, customMixin);
}
if (options.xAxisMin) {
retval = mixin(retval, {
scales: {
xAxes: [{
time: {
min: options.xAxisMin
}
}],
}
}, true, customMixin);
}
}
}
retval.legend = <Chart.ChartLegendOptions>{
position: options.legendPosition as Chart.PositionType,
display: options.legendPosition !== LegendPosition.None,
labels: {
fontColor: foreground
}
};
}
// these are custom options that will throw compile errors
(<any>retval).viewArea = {
backgroundColor: background
};
return retval;
}
public set options(options: IInsightOptions) {
this._options = options;
this.originalType = options.type as ChartType;
if (this.options.type === ChartType.TimeSeries) {
this.options.type = ChartType.Line;
this.options.dataType = DataType.Point;
this.options.dataDirection = DataDirection.Horizontal;
}
this.data = this._data;
}
public get options(): IInsightOptions {
return this._options;
}
}
/**
* The Following code is pulled from ng2-charting in order to keep the same
* color functionality
*/
const defaultColors: Array<Color> = [
[255, 99, 132],
[54, 162, 235],
[255, 206, 86],
[231, 233, 237],
[75, 192, 192],
[151, 187, 205],
[220, 220, 220],
[247, 70, 74],
[70, 191, 189],
[253, 180, 92],
[148, 159, 177],
[77, 83, 96]
];
type Color = [number, number, number];
interface ILineColor {
backgroundColor: string;
borderColor: string;
pointBackgroundColor: string;
pointBorderColor: string;
pointHoverBackgroundColor: string;
pointHoverBorderColor: string;
}
interface IBarColor {
backgroundColor: string;
borderColor: string;
hoverBackgroundColor: string;
hoverBorderColor: string;
}
interface IPieColors {
backgroundColor: Array<string>;
borderColor: Array<string>;
pointBackgroundColor: Array<string>;
pointBorderColor: Array<string>;
pointHoverBackgroundColor: Array<string>;
pointHoverBorderColor: Array<string>;
}
interface IPolarAreaColors {
backgroundColor: Array<string>;
borderColor: Array<string>;
hoverBackgroundColor: Array<string>;
hoverBorderColor: Array<string>;
}
function rgba(colour: Color, alpha: number): string {
return 'rgba(' + colour.concat(alpha).join(',') + ')';
}
function getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomColor(): Color {
return [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)];
}
/**
* Generate colors for line|bar charts
*/
function generateColor(index: number): Color {
return defaultColors[index] || getRandomColor();
}
/**
* Generate colors for pie|doughnut charts
*/
function generateColors(count: number): Array<Color> {
const colorsArr = new Array(count);
for (let i = 0; i < count; i++) {
colorsArr[i] = defaultColors[i] || getRandomColor();
}
return colorsArr;
}
function formatLineColor(colors: Color): ILineColor {
return {
backgroundColor: rgba(colors, 0.4),
borderColor: rgba(colors, 1),
pointBackgroundColor: rgba(colors, 1),
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: rgba(colors, 0.8)
};
}
function formatBarColor(colors: Color): IBarColor {
return {
backgroundColor: rgba(colors, 0.6),
borderColor: rgba(colors, 1),
hoverBackgroundColor: rgba(colors, 0.8),
hoverBorderColor: rgba(colors, 1)
};
}
function formatPieColors(colors: Array<Color>): IPieColors {
return {
backgroundColor: colors.map(color => rgba(color, 0.6)),
borderColor: colors.map(() => '#fff'),
pointBackgroundColor: colors.map(color => rgba(color, 1)),
pointBorderColor: colors.map(() => '#fff'),
pointHoverBackgroundColor: colors.map(color => rgba(color, 1)),
pointHoverBorderColor: colors.map(color => rgba(color, 1))
};
}
function formatPolarAreaColors(colors: Array<Color>): IPolarAreaColors {
return {
backgroundColor: colors.map(color => rgba(color, 0.6)),
borderColor: colors.map(color => rgba(color, 1)),
hoverBackgroundColor: colors.map(color => rgba(color, 0.8)),
hoverBorderColor: colors.map(color => rgba(color, 1))
};
}
/**
* Generate colors by chart type
*/
function getColors(chartType: string, index: number, count: number): Color | ILineColor | IBarColor | IPieColors | IPolarAreaColors {
if (chartType === 'pie' || chartType === 'doughnut') {
return formatPieColors(generateColors(count));
}
if (chartType === 'polarArea') {
return formatPolarAreaColors(generateColors(count));
}
if (chartType === 'line' || chartType === 'radar') {
return formatLineColor(generateColor(index));
}
if (chartType === 'bar' || chartType === 'horizontalBar') {
return formatBarColor(generateColor(index));
}
return generateColor(index);
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInsight, IInsightData } from './interfaces';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { $ } from 'vs/base/browser/dom';
import { mixin } from 'vs/base/common/objects';
import { IInsightOptions, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
import * as nls from 'vs/nls';
import { startsWith } from 'vs/base/common/strings';
export interface IConfig extends IInsightOptions {
encoding?: string;
imageFormat?: string;
}
const defaultConfig: IConfig = {
type: InsightType.Image,
encoding: 'hex',
imageFormat: 'jpeg'
};
export class ImageInsight implements IInsight {
public static readonly types = [InsightType.Image];
public readonly types = ImageInsight.types;
private _options: IConfig;
private imageEle: HTMLImageElement;
constructor(container: HTMLElement, options: IConfig, @INotificationService private _notificationService: INotificationService) {
this._options = mixin(options, defaultConfig, false);
this.imageEle = $('img');
container.appendChild(this.imageEle);
}
public layout() {
}
public dispose() {
}
set options(config: IConfig) {
this._options = mixin(config, defaultConfig, false);
}
get options(): IConfig {
return this._options;
}
set data(data: IInsightData) {
const that = this;
if (data.rows && data.rows.length > 0 && data.rows[0].length > 0) {
let img = data.rows[0][0];
if (this._options.encoding === 'hex') {
img = ImageInsight._hexToBase64(img);
}
this.imageEle.onerror = function () {
this.src = require.toUrl(`./media/images/invalidImage.png`);
that._notificationService.error(nls.localize('invalidImage', "Table does not contain a valid image"));
};
this.imageEle.src = `data:image/${this._options.imageFormat};base64,${img}`;
}
}
private static _hexToBase64(hexVal: string) {
if (startsWith(hexVal, '0x')) {
hexVal = hexVal.slice(2);
}
// should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64')
return btoa(String.fromCharCode.apply(null, hexVal.replace(/\r|\n/g, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ').map(v => Number(v))));
}
}

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Graph } from './graphInsight';
import { ImageInsight } from './imageInsight';
import { TableInsight } from './tableInsight';
import { IInsight, IInsightCtor, IInsightData } from './interfaces';
import { CountInsight } from './countInsight';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Dimension, clearNode } from 'vs/base/browser/dom';
import { deepClone } from 'vs/base/common/objects';
import { IInsightOptions, ChartType, DataDirection, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
import { find } from 'vs/base/common/arrays';
const defaultOptions: IInsightOptions = {
type: ChartType.Bar,
dataDirection: DataDirection.Horizontal
};
export class Insight {
private _insight: IInsight;
public get insight(): IInsight {
return this._insight;
}
private _options: IInsightOptions;
private _data: IInsightData;
private dim: Dimension;
constructor(
private container: HTMLElement, options: IInsightOptions = defaultOptions,
@IInstantiationService private _instantiationService: IInstantiationService
) {
this.options = options;
this.buildInsight();
}
public layout(dim: Dimension) {
this.dim = dim;
this.insight.layout(dim);
}
public set options(val: IInsightOptions) {
this._options = deepClone(val);
if (this.insight) {
// check to see if we need to change the insight type
if (!find(this.insight.types, x => x === this.options.type)) {
this.buildInsight();
} else {
this.insight.options = this.options;
}
}
}
public get options(): IInsightOptions {
return this._options;
}
public set data(val: IInsightData) {
this._data = val;
if (this.insight) {
this.insight.data = val;
}
}
private buildInsight() {
if (this.insight) {
this.insight.dispose();
}
clearNode(this.container);
let ctor = this.findctor(this.options.type);
if (ctor) {
this._insight = this._instantiationService.createInstance(ctor, this.container, this.options);
this.insight.layout(this.dim);
if (this._data) {
this.insight.data = this._data;
}
}
}
public get isCopyable(): boolean {
return !!find(Graph.types, x => x === this.options.type as ChartType);
}
private findctor(type: ChartType | InsightType): IInsightCtor {
if (find(Graph.types, x => x === type as ChartType)) {
return Graph;
} else if (find(ImageInsight.types, x => x === type as InsightType)) {
return ImageInsight;
} else if (find(TableInsight.types, x => x === type as InsightType)) {
return TableInsight;
} else if (find(CountInsight.types, x => x === type as InsightType)) {
return CountInsight;
}
return undefined;
}
}

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension } from 'vs/base/browser/dom';
import { mixin } from 'sql/base/common/objects';
import * as types from 'vs/base/common/types';
import { IInsightOptions, InsightType, ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
export interface IPointDataSet {
data: Array<{ x: number | string, y: number }>;
label?: string;
fill: boolean;
backgroundColor?: string;
}
export function customMixin(destination: any, source: any, overwrite?: boolean): any {
if (types.isObject(source)) {
mixin(destination, source, overwrite, customMixin);
} else if (types.isArray(source)) {
for (let i = 0; i < source.length; i++) {
if (destination[i]) {
mixin(destination[i], source[i], overwrite, customMixin);
} else {
destination[i] = source[i];
}
}
} else {
destination = source;
}
return destination;
}
export interface IInsightData {
columns: Array<string>;
rows: Array<Array<string>>;
}
export interface IInsight {
options: IInsightOptions;
data: IInsightData;
readonly types: Array<InsightType | ChartType>;
layout(dim: Dimension);
dispose();
}
export interface IInsightCtor {
new(container: HTMLElement, options: IInsightOptions, ...services: { _serviceBrand: undefined; }[]): IInsight;
readonly types: Array<InsightType | ChartType>;
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.chart-parent-container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.actionbar-container {
width: 100%;
flex: 0 0 auto;
}
.charting-container {
display: flex;
flex: 1 1 auto;
}
.insight-container {
flex: 1 1 0;
}
.options-container {
width: 250px;
padding-right: 10px;
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.count-label-container {
margin-left: 5px;
}
.label-container {
font-size: 20px
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInsight, IInsightData } from './interfaces';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { Table } from 'sql/base/browser/ui/table/table';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
import { $, Dimension } from 'vs/base/browser/dom';
import { Disposable } from 'vs/base/common/lifecycle';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
export class TableInsight extends Disposable implements IInsight {
public static readonly types = [InsightType.Table];
public readonly types = TableInsight.types;
private table: Table<any>;
private dataView: TableDataView<any>;
private columns: Slick.Column<any>[];
constructor(container: HTMLElement, options: any,
@IThemeService themeService: IThemeService
) {
super();
let tableContainer = $('div');
tableContainer.style.width = '100%';
tableContainer.style.height = '100%';
container.appendChild(tableContainer);
this.dataView = new TableDataView();
this.table = new Table(tableContainer, { dataProvider: this.dataView }, { showRowNumber: true });
this.table.setSelectionModel(new CellSelectionModel());
this._register(attachTableStyler(this.table, themeService));
}
set data(data: IInsightData) {
this.dataView.clear();
this.dataView.push(transformData(data.rows, data.columns));
this.columns = transformColumns(data.columns);
this.table.columns = this.columns;
}
layout(dim: Dimension) {
this.table.layout(dim);
}
public options;
}
function transformData(rows: string[][], columns: string[]): { [key: string]: string }[] {
return rows.map(row => {
let object: { [key: string]: string } = {};
row.forEach((val, index) => {
object[columns[index]] = val;
});
return object;
});
}
function transformColumns(columns: string[]): Slick.Column<any>[] {
return columns.map(col => {
return <Slick.Column<any>>{
name: col,
id: col,
field: col
};
});
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class ChartState {
dataId: { batchId: number, resultId: number };
options: IInsightOptions = {
type: ChartType.Bar
};
dispose() {
}
}
export interface IInsightOptions {
type: InsightType | ChartType;
dataDirection?: DataDirection;
dataType?: DataType;
labelFirstColumn?: boolean;
columnsAsLabels?: boolean;
legendPosition?: LegendPosition;
yAxisLabel?: string;
yAxisMin?: number;
yAxisMax?: number;
xAxisLabel?: string;
xAxisMin?: number;
xAxisMax?: number;
encoding?: string;
imageFormat?: string;
}
export enum InsightType {
Image = 'image',
Table = 'table',
Count = 'count'
}
export enum ChartType {
Bar = 'bar',
Doughnut = 'doughnut',
HorizontalBar = 'horizontalBar',
Line = 'line',
Pie = 'pie',
TimeSeries = 'timeSeries',
Scatter = 'scatter'
}
export enum LegendPosition {
Top = 'top',
Bottom = 'bottom',
Left = 'left',
Right = 'right',
None = 'none'
}
export enum DataType {
Number = 'number',
Point = 'point'
}
export enum DataDirection {
Vertical = 'vertical',
Horizontal = 'horizontal'
}

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ChartView } from 'sql/workbench/contrib/charts/browser/chartView';
import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService';
import { TestLayoutService } from 'vs/workbench/test/workbenchTestServices';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IThemeService } from 'vs/platform/theme/common/themeService';
suite('Chart View', () => {
test('initializes without error', () => {
const chartview = createChartView();
assert(chartview);
});
test('renders without error', () => {
const chartview = createChartView();
chartview.render(document.createElement('div'));
});
});
function createChartView(): ChartView {
const layoutService = new TestLayoutService();
const contextViewService = new ContextViewService(layoutService);
const themeService = new TestThemeService();
const instantiationService = new TestInstantiationService();
instantiationService.stub(IThemeService, themeService);
return new ChartView(contextViewService, themeService, instantiationService);
}

View File

@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { CommandLineWorkbenchContribution } from 'sql/workbench/contrib/commandLine/electron-browser/commandLine';
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(CommandLineWorkbenchContribution, LifecyclePhase.Restored);

View File

@@ -0,0 +1,261 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as querystring from 'querystring';
import * as azdata from 'azdata';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import * as Constants from 'sql/platform/connection/common/constants';
import * as platform from 'vs/platform/registry/common/platform';
import { IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/contrib/connection/common/connectionProviderExtension';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ipcRenderer as ipc } from 'electron';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { localize } from 'vs/nls';
import { QueryInput } from 'sql/workbench/contrib/query/common/queryInput';
import { URI } from 'vs/base/common/uri';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
import { IURLService, IURLHandler } from 'vs/platform/url/common/url';
import { getErrorMessage } from 'vs/base/common/errors';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { find } from 'vs/base/common/arrays';
const connectAuthority = 'connect';
interface SqlArgs {
_?: string[];
aad?: boolean;
database?: string;
integrated?: boolean;
server?: string;
user?: string;
command?: string;
provider?: string;
}
export class CommandLineWorkbenchContribution implements IWorkbenchContribution, IURLHandler {
constructor(
@ICapabilitiesService private readonly _capabilitiesService: ICapabilitiesService,
@IConnectionManagementService private readonly _connectionManagementService: IConnectionManagementService,
@IEnvironmentService environmentService: IEnvironmentService,
@IEditorService private readonly _editorService: IEditorService,
@ICommandService private readonly _commandService: ICommandService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@INotificationService private readonly _notificationService: INotificationService,
@ILogService private readonly logService: ILogService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IURLService urlService: IURLService,
@IDialogService private readonly dialogService: IDialogService
) {
if (ipc) {
ipc.on('ads:processCommandLine', (event: any, args: ParsedArgs) => this.onLaunched(args));
}
// we only get the ipc from main during window reuse
if (environmentService) {
this.onLaunched(environmentService.args);
}
if (urlService) {
urlService.registerHandler(this);
}
}
private onLaunched(args: ParsedArgs) {
const registry = platform.Registry.as<IConnectionProviderRegistry>(ConnectionProviderExtensions.ConnectionProviderContributions);
let sqlProvider = registry.getProperties(Constants.mssqlProviderName);
// We can't connect to object explorer until the MSSQL connection provider is registered
if (sqlProvider) {
this.processCommandLine(args).catch(reason => { this.logService.warn('processCommandLine failed: ' + reason); });
} else {
registry.onNewProvider(e => {
if (e.id === Constants.mssqlProviderName) {
this.processCommandLine(args).catch(reason => { this.logService.warn('processCommandLine failed: ' + reason); });
}
});
}
}
// We base our logic on the combination of (server, command) values.
// (serverName, commandName) => Connect object explorer and execute the command, passing the connection profile to the command. Do not load query editor.
// (null, commandName) => Launch the command with a null connection. If the command implementation needs a connection, it will need to create it.
// (serverName, null) => Connect object explorer and open a new query editor if no file names are passed. If file names are passed, connect their editors to the server.
// (null, null) => Prompt for a connection unless there are registered servers
public async processCommandLine(args: SqlArgs): Promise<void> {
let profile: IConnectionProfile = undefined;
let commandName = undefined;
if (args) {
if (this._commandService) {
commandName = args.command;
}
if (args.server) {
profile = this.readProfileFromArgs(args);
}
}
let showConnectDialogOnStartup: boolean = this._configurationService.getValue('workbench.showConnectDialogOnStartup');
if (showConnectDialogOnStartup && !commandName && !profile && !this._connectionManagementService.hasRegisteredServers()) {
// prompt the user for a new connection on startup if no profiles are registered
await this._connectionManagementService.showConnectionDialog();
return;
}
let connectedContext: azdata.ConnectedContext = undefined;
if (profile) {
if (this._notificationService) {
this._notificationService.status(localize('connectingLabel', "Connecting: {0}", profile.serverName), { hideAfter: 2500 });
}
try {
await this._connectionManagementService.connectIfNotConnected(profile, 'connection', true);
// Before sending to extensions, we should a) serialize to IConnectionProfile or things will fail,
// and b) use the latest version of the profile from the service so most fields are filled in.
let updatedProfile = this._connectionManagementService.getConnectionProfileById(profile.id);
connectedContext = { connectionProfile: new ConnectionProfile(this._capabilitiesService, updatedProfile).toIConnectionProfile() };
} catch (err) {
this.logService.warn('Failed to connect due to error' + getErrorMessage(err));
}
}
if (commandName) {
if (this._notificationService) {
this._notificationService.status(localize('runningCommandLabel', "Running command: {0}", commandName), { hideAfter: 2500 });
}
await this._commandService.executeCommand(commandName, connectedContext);
} else if (profile) {
// If we were given a file and it was opened with the sql editor,
// we want to connect the given profile to to it.
// If more than one file was passed, only show the connection dialog error on one of them.
if (args._ && args._.length > 0) {
await Promise.all(args._.map((f, i) => this.processFile(URI.file(f).toString(), profile, i === 0)));
}
else {
// Default to showing new query
if (this._notificationService) {
this._notificationService.status(localize('openingNewQueryLabel', "Opening new query: {0}", profile.serverName), { hideAfter: 2500 });
}
try {
await this.instantiationService.invokeFunction(openNewQuery, profile);
} catch (error) {
this.logService.warn('unable to open query editor ' + error);
// Note: we are intentionally swallowing this error.
// In part this is to accommodate unit testing where we don't want to set up the query stack
}
}
}
}
public async handleURL(uri: URI): Promise<boolean> {
// Catch file URLs
let authority = uri.authority.toLowerCase();
if (authority === connectAuthority) {
try {
let args = this.parseProtocolArgs(uri);
if (!args.server) {
this._notificationService.warn(localize('warnServerRequired', "Cannot connect as no server information was provided"));
return true;
}
let isOpenOk = await this.confirmConnect(args);
if (isOpenOk) {
await this.processCommandLine(args);
}
} catch (err) {
this._notificationService.error(localize('errConnectUrl', "Could not open URL due to error {0}", getErrorMessage(err)));
}
// Handled either way
return true;
}
return false;
}
private async confirmConnect(args: SqlArgs): Promise<boolean> {
let detail = args && args.server ? localize('connectServerDetail', "This will connect to server {0}", args.server) : '';
const result = await this.dialogService.confirm({
message: localize('confirmConnect', "Are you sure you want to connect?"),
detail: detail,
primaryButton: localize('open', "&&Open"),
type: 'question'
});
if (result.confirmed) {
return true;
}
return false;
}
private parseProtocolArgs(uri: URI): SqlArgs {
let args: SqlArgs = querystring.parse(uri.query);
// Clear out command, not supporting arbitrary command via this path
args.command = undefined;
return args;
}
// If an open and connectable query editor exists for the given URI, attach it to the connection profile
private async processFile(uriString: string, profile: IConnectionProfile, warnOnConnectFailure: boolean): Promise<void> {
let activeEditor = this._editorService.editors.filter(v => v.getResource().toString() === uriString).pop();
if (activeEditor) {
let queryInput = activeEditor as QueryInput;
if (queryInput && queryInput.state.connected) {
let options: IConnectionCompletionOptions = {
params: { connectionType: ConnectionType.editor, runQueryOnCompletion: RunQueryOnConnectionMode.none, input: queryInput },
saveTheConnection: false,
showDashboard: false,
showConnectionDialogOnError: warnOnConnectFailure,
showFirewallRuleOnError: warnOnConnectFailure
};
if (this._notificationService) {
this._notificationService.status(localize('connectingQueryLabel', "Connecting query file"), { hideAfter: 2500 });
}
await this._connectionManagementService.connect(profile, uriString, options);
}
}
}
private readProfileFromArgs(args: SqlArgs) {
let profile = new ConnectionProfile(this._capabilitiesService, null);
// We want connection store to use any matching password it finds
profile.savePassword = true;
profile.providerName = args.provider ? args.provider : Constants.mssqlProviderName;
profile.serverName = args.server;
profile.databaseName = args.database ? args.database : '';
profile.userName = args.user ? args.user : '';
profile.authenticationType = args.integrated ? Constants.integrated : args.aad ? Constants.azureMFA : (profile.userName.length > 0) ? Constants.sqlLogin : Constants.integrated;
profile.connectionName = '';
profile.setOptionValue('applicationName', Constants.applicationName);
profile.setOptionValue('databaseDisplayName', profile.databaseName);
profile.setOptionValue('groupId', profile.groupId);
return this._connectionManagementService ? this.tryMatchSavedProfile(profile) : profile;
}
private tryMatchSavedProfile(profile: ConnectionProfile) {
let match: ConnectionProfile = undefined;
// If we can find a saved mssql provider connection that matches the args, use it
let groups = this._connectionManagementService.getConnectionGroups([Constants.mssqlProviderName]);
if (groups && groups.length > 0) {
let rootGroup = groups[0];
let connections = ConnectionProfileGroup.getConnectionsInGroup(rootGroup);
match = find(connections, (c) => this.matchProfile(profile, c));
}
return match ? match : profile;
}
// determines if the 2 profiles are a functional match
// profile1 is the profile generated from command line parameters
private matchProfile(profile1: ConnectionProfile, profile2: ConnectionProfile): boolean {
return equalsIgnoreCase(profile1.serverName, profile2.serverName)
&& equalsIgnoreCase(profile1.providerName, profile2.providerName)
// case sensitive servers can have 2 databases whose name differs only in case
&& profile1.databaseName === profile2.databaseName
&& equalsIgnoreCase(profile1.userName, profile2.userName)
&& profile1.authenticationType === profile2.authenticationType;
}
}

View File

@@ -0,0 +1,558 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import * as azdata from 'azdata';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { CommandLineWorkbenchContribution } from 'sql/workbench/contrib/commandLine/electron-browser/commandLine';
import * as Constants from 'sql/platform/connection/common/constants';
import { ParsedArgs } from 'vs/platform/environment/common/environment';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType } from 'sql/platform/connection/common/connectionManagement';
import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { TestCommandService } from 'vs/editor/test/browser/editorTestServices';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { assertThrowsAsync } from 'sql/base/test/common/async';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestEditorService, TestDialogService } from 'vs/workbench/test/workbenchTestServices';
import { QueryInput, QueryEditorState } from 'sql/workbench/contrib/query/common/queryInput';
import { URI } from 'vs/base/common/uri';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { isUndefinedOrNull } from 'vs/base/common/types';
class TestParsedArgs implements ParsedArgs {
[arg: string]: any;
_: string[];
aad?: boolean;
add?: boolean;
database?: string;
command?: string;
debugBrkPluginHost?: string;
debugBrkSearch?: string;
debugId?: string;
debugPluginHost?: string;
debugSearch?: string;
diff?: boolean;
'disable-crash-reporter'?: boolean;
'disable-extension'?: string[]; // undefined or array of 1 or more
'disable-extensions'?: boolean;
'disable-restore-windows'?: boolean;
'disable-telemetry'?: boolean;
'disable-updates'?: boolean;
'driver'?: string;
'enable-proposed-api'?: string[];
'export-default-configuration'?: string;
'extensions-dir'?: string;
extensionDevelopmentPath?: string[];
extensionTestsPath?: string;
'file-chmod'?: boolean;
'file-write'?: boolean;
'folder-uri'?: string[];
goto?: boolean;
help?: boolean;
'install-extension'?: string[];
'install-source'?: string;
integrated?: boolean;
'list-extensions'?: boolean;
locale?: string;
log?: string;
logExtensionHostCommunication?: boolean;
'max-memory'?: string;
'new-window'?: boolean;
'open-url'?: boolean;
performance?: boolean;
'prof-append-timers'?: string;
'prof-startup'?: boolean;
'prof-startup-prefix'?: string;
'reuse-window'?: boolean;
server?: string;
'show-versions'?: boolean;
'skip-add-to-recently-opened'?: boolean;
'skip-getting-started'?: boolean;
'skip-release-notes'?: boolean;
status?: boolean;
'sticky-quickopen'?: boolean;
'uninstall-extension'?: string[];
'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch.
'upload-logs'?: string;
user?: string;
'user-data-dir'?: string;
_urls?: string[];
verbose?: boolean;
version?: boolean;
wait?: boolean;
waitMarkerFilePath?: string;
}
suite('commandLineService tests', () => {
let capabilitiesService: TestCapabilitiesService;
setup(() => {
capabilitiesService = new TestCapabilitiesService();
});
function getCommandLineContribution(
connectionManagementService: IConnectionManagementService,
configurationService: IConfigurationService,
capabilitiesService?: ICapabilitiesService,
commandService?: ICommandService,
editorService?: IEditorService,
logService?: ILogService,
dialogService?: IDialogService,
notificationService?: INotificationService
): CommandLineWorkbenchContribution {
return new CommandLineWorkbenchContribution(
capabilitiesService,
connectionManagementService,
undefined,
editorService,
commandService,
configurationService,
notificationService,
logService,
undefined,
undefined,
dialogService
);
}
function getConfigurationServiceMock(showConnectDialogOnStartup: boolean): TypeMoq.Mock<IConfigurationService> {
let configurationService = TypeMoq.Mock.ofType<IConfigurationService>(TestConfigurationService);
configurationService.setup((c) => c.getValue(TypeMoq.It.isAnyString())).returns((config: string) => showConnectDialogOnStartup);
return configurationService;
}
test('processCommandLine shows connection dialog by default', done => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.setup((c) => c.showConnectionDialog())
.returns(() => new Promise<void>((resolve, reject) => { resolve(); }))
.verifiable();
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => new Promise<string>((resolve, reject) => { resolve('unused'); }))
.verifiable(TypeMoq.Times.never());
const configurationService = getConfigurationServiceMock(true);
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object);
contribution.processCommandLine(new TestParsedArgs()).then(() => {
connectionManagementService.verifyAll();
done();
}, error => { assert.fail(error, null, 'processCommandLine rejected ' + error); done(); });
});
test('processCommandLine does nothing if no server name and command name is provided and the configuration \'workbench.showConnectDialogOnStartup\' is set to false, even if registered servers exist', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.verifiable(TypeMoq.Times.never());
const configurationService = getConfigurationServiceMock(false);
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object);
await contribution.processCommandLine(new TestParsedArgs());
connectionManagementService.verifyAll();
});
test('processCommandLine does nothing if registered servers exist and no server name is provided', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => new Promise<string>((resolve, reject) => { resolve('unused'); }))
.verifiable(TypeMoq.Times.never());
const configurationService = getConfigurationServiceMock(true);
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object);
try {
await contribution.processCommandLine(new TestParsedArgs());
connectionManagementService.verifyAll();
} catch (error) {
assert.fail(error, null, 'processCommandLine rejected ' + error);
}
});
test('processCommandLine opens a new connection if a server name is passed', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const args: TestParsedArgs = new TestParsedArgs();
args.server = 'myserver';
args.database = 'mydatabase';
args.user = 'myuser';
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
let originalProfile: IConnectionProfile = undefined;
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
.returns((conn) => {
originalProfile = conn;
return Promise.resolve('unused');
})
.verifiable(TypeMoq.Times.once());
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
const configurationService = getConfigurationServiceMock(true);
const logService = new NullLogService();
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService);
await contribution.processCommandLine(args);
connectionManagementService.verifyAll();
});
test('processCommandLine invokes a command without a profile parameter when no server is passed', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Loose);
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
const args: TestParsedArgs = new TestParsedArgs();
args.command = 'mycommand';
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.verifiable(TypeMoq.Times.never());
let capturedArgs: any;
commandService.setup(c => c.executeCommand(TypeMoq.It.isAnyString(), undefined))
.returns((command, args) => {
capturedArgs = args;
return Promise.resolve();
})
.verifiable(TypeMoq.Times.once());
const configurationService = getConfigurationServiceMock(true);
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object);
await contribution.processCommandLine(args);
connectionManagementService.verifyAll();
commandService.verifyAll();
assert(isUndefinedOrNull(capturedArgs));
});
test('processCommandLine invokes a command with a profile parameter when a server is passed', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
const args: TestParsedArgs = new TestParsedArgs();
args.command = 'mycommand';
args.server = 'myserver';
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
let originalProfile: IConnectionProfile = undefined;
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver'), 'connection', true))
.returns((conn) => {
originalProfile = conn;
return Promise.resolve('unused');
})
.verifiable(TypeMoq.Times.once());
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
let actualProfile: azdata.ConnectedContext = undefined;
commandService.setup(c => c.executeCommand('mycommand', TypeMoq.It.isAny()))
.returns((cmdName, profile) => {
actualProfile = profile;
return Promise.resolve();
})
.verifiable(TypeMoq.Times.once());
const configurationService = getConfigurationServiceMock(true);
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object);
await contribution.processCommandLine(args);
connectionManagementService.verifyAll();
commandService.verifyAll();
assert(!isUndefinedOrNull(actualProfile));
assert.equal(actualProfile.connectionProfile.serverName, args.server);
});
test('processCommandLine rejects unknown commands', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
const args: TestParsedArgs = new TestParsedArgs();
args.command = 'mycommand';
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
commandService.setup(c => c.executeCommand('mycommand'))
.returns(() => Promise.reject(new Error('myerror')))
.verifiable(TypeMoq.Times.once());
const configurationService = getConfigurationServiceMock(true);
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object);
assertThrowsAsync(async () => await contribution.processCommandLine(args));
});
test('processCommandLine uses Integrated auth if no user name or auth type is passed', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const args: TestParsedArgs = new TestParsedArgs();
args.server = 'myserver';
args.database = 'mydatabase';
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
let originalProfile: IConnectionProfile = undefined;
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.integrated), 'connection', true))
.returns((conn) => {
originalProfile = conn;
return Promise.resolve('unused');
})
.verifiable(TypeMoq.Times.once());
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
const configurationService = getConfigurationServiceMock(true);
const logService = new NullLogService();
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService);
await contribution.processCommandLine(args);
connectionManagementService.verifyAll();
});
test('processCommandLine reuses saved connections that match args', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
let connection = new ConnectionProfile(capabilitiesService, {
connectionName: 'Test',
savePassword: false,
groupFullName: 'testGroup',
serverName: 'myserver',
databaseName: 'mydatabase',
authenticationType: Constants.integrated,
password: undefined,
userName: '',
groupId: undefined,
providerName: 'MSSQL',
options: {},
saveProfile: true,
id: 'testID'
});
let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined);
conProfGroup.connections = [connection];
const args: TestParsedArgs = new TestParsedArgs();
args.server = 'myserver';
args.database = 'mydatabase';
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
let originalProfile: IConnectionProfile = undefined;
connectionManagementService.setup(c => c.connectIfNotConnected(
TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.integrated && p.connectionName === 'Test' && p.id === 'testID'), 'connection', true))
.returns((conn) => {
originalProfile = conn;
return Promise.resolve('unused');
})
.verifiable(TypeMoq.Times.once());
connectionManagementService.setup(c => c.getConnectionProfileById('testID')).returns(() => originalProfile).verifiable(TypeMoq.Times.once());
connectionManagementService.setup(x => x.getConnectionGroups(TypeMoq.It.isAny())).returns(() => [conProfGroup]);
const configurationService = getConfigurationServiceMock(true);
const logService = new NullLogService();
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService);
await contribution.processCommandLine(args);
connectionManagementService.verifyAll();
});
test('processCommandLine connects opened query files to given server', async () => {
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const args: TestParsedArgs = new TestParsedArgs();
args.server = 'myserver';
args.database = 'mydatabase';
args.user = 'myuser';
args._ = ['c:\\dir\\file.sql'];
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
let originalProfile: IConnectionProfile = undefined;
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
.returns((conn) => {
originalProfile = conn;
return Promise.resolve('unused');
}).verifiable(TypeMoq.Times.once());
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
const configurationService = getConfigurationServiceMock(true);
const queryInput: TypeMoq.Mock<QueryInput> = TypeMoq.Mock.ofType<QueryInput>(QueryInput);
let uri = URI.file(args._[0]);
const queryState = new QueryEditorState();
queryState.connected = true;
queryInput.setup(q => q.state).returns(() => queryState);
queryInput.setup(q => q.getResource()).returns(() => uri).verifiable(TypeMoq.Times.once());
const editorService: TypeMoq.Mock<IEditorService> = TypeMoq.Mock.ofType<IEditorService>(TestEditorService, TypeMoq.MockBehavior.Strict);
editorService.setup(e => e.editors).returns(() => [queryInput.object]);
connectionManagementService.setup(c =>
c.connect(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin),
uri.toString(),
TypeMoq.It.is<IConnectionCompletionOptions>(i => i.params.input === queryInput.object && i.params.connectionType === ConnectionType.editor))
).verifiable(TypeMoq.Times.once());
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, editorService.object);
await contribution.processCommandLine(args);
queryInput.verifyAll();
connectionManagementService.verifyAll();
});
suite('URL Handler', () => {
let dialogService: TypeMoq.Mock<TestDialogService>;
setup(() => {
dialogService = TypeMoq.Mock.ofType(TestDialogService);
});
test('handleUrl ignores non-connect URLs', async () => {
// Given a URI pointing to a server
let uri: URI = URI.parse('azuredatastudio://file?server=myserver&database=mydatabase&user=myuser');
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const configurationService = getConfigurationServiceMock(true);
const logService = new NullLogService();
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService, dialogService.object);
// When I call the URL handler and user confirms they should connect
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: true }));
let result = await contribution.handleURL(uri);
// Then I expect connection management service to have been called
assert.equal(result, false, 'Expected URL to be ignored');
});
test('handleUrl opens a new connection if a server name is passed', async () => {
// Given a URI pointing to a server
let uri: URI = URI.parse('azuredatastudio://connect?server=myserver&database=mydatabase&user=myuser');
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
let originalProfile: IConnectionProfile = undefined;
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
.returns((conn) => {
originalProfile = conn;
return Promise.resolve('unused');
})
.verifiable(TypeMoq.Times.once());
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
const configurationService = getConfigurationServiceMock(true);
const logService = new NullLogService();
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService, dialogService.object);
// When I call the URL handler and user confirms they should connect
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: true }));
let result = await contribution.handleURL(uri);
// Then I expect connection management service to have been called
assert.equal(result, true, 'Expected URL to be handled');
connectionManagementService.verifyAll();
});
test('handleUrl does nothing if a user does not confirm', async () => {
// Given a URI pointing to a server
let uri: URI = URI.parse('azuredatastudio://connect?server=myserver&database=mydatabase&user=myuser');
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
let originalProfile: IConnectionProfile = undefined;
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
.returns((conn) => {
originalProfile = conn;
return Promise.resolve('unused');
})
// Note: should not run since we expect to cancel before this
.verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
const configurationService = getConfigurationServiceMock(true);
const logService = new NullLogService();
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService, dialogService.object);
// When I call the URL handler and user says no on confirmation dialog
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: false }));
let result = await contribution.handleURL(uri);
// Then I expect no connection, but the URL should still be handled
assert.equal(result, true, 'Expected URL to be handled');
connectionManagementService.verifyAll();
});
test('handleUrl ignores commands', async () => {
// Given I pass a command
let uri: URI = URI.parse('azuredatastudio://connect?command=mycommand');
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
commandService.setup(c => c.executeCommand('mycommand'))
.returns(() => Promise.resolve())
.verifiable(TypeMoq.Times.never());
const configurationService = getConfigurationServiceMock(true);
const notificationService = TypeMoq.Mock.ofType(TestNotificationService);
notificationService.setup(n => n.warn(TypeMoq.It.isAny())).returns(() => undefined)
.verifiable(TypeMoq.Times.once());
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object, undefined, new NullLogService(), dialogService.object, notificationService.object);
// When I handle the command URL
let result = await contribution.handleURL(uri);
// Then command service should not have been called, and instead connection should be handled
assert.equal(result, true);
commandService.verifyAll();
notificationService.verifyAll();
});
test('handleUrl ignores commands and connects', async () => {
// Given I pass a command
let uri: URI = URI.parse('azuredatastudio://connect?command=mycommand&server=myserver&database=mydatabase&user=myuser');
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
.returns((conn) => {
return Promise.resolve('unused');
})
.verifiable(TypeMoq.Times.once());
commandService.setup(c => c.executeCommand('mycommand'))
.returns(() => Promise.resolve())
.verifiable(TypeMoq.Times.never());
const configurationService = getConfigurationServiceMock(true);
const notificationService = TypeMoq.Mock.ofType(TestNotificationService);
notificationService.setup(n => n.warn(TypeMoq.It.isAny())).returns(() => undefined)
.verifiable(TypeMoq.Times.never());
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object, undefined, new NullLogService(), dialogService.object, notificationService.object);
// When I handle the command URL
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: true }));
let result = await contribution.handleURL(uri);
// Then command service should not have been called, and instead connection should be handled
assert.equal(result, true);
commandService.verifyAll();
notificationService.verifyAll();
connectionManagementService.verifyAll();
});
});
});

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OptionsDialog } from 'sql/workbench/browser/modal/optionsDialog';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as azdata from 'azdata';
import { localize } from 'vs/nls';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
export class AdvancedPropertiesController {
private _advancedDialog: OptionsDialog;
private _options: { [name: string]: any };
constructor(private _onCloseAdvancedProperties: () => void,
@IInstantiationService private _instantiationService: IInstantiationService
) {
}
private handleOnOk(): void {
this._options = this._advancedDialog.optionValues;
}
public showDialog(providerOptions: azdata.ConnectionOption[], options: { [name: string]: any }): void {
this._options = options;
let serviceOptions = providerOptions.map(option => AdvancedPropertiesController.connectionOptionToServiceOption(option));
this.advancedDialog.open(serviceOptions, this._options);
}
public get advancedDialog() {
if (!this._advancedDialog) {
this._advancedDialog = this._instantiationService.createInstance(
OptionsDialog, localize('connectionAdvancedProperties', "Advanced Properties"), TelemetryKeys.ConnectionAdvancedProperties, { hasBackButton: true, cancelLabel: localize('advancedProperties.discard', "Discard") });
this._advancedDialog.onCloseEvent(() => this._onCloseAdvancedProperties());
this._advancedDialog.onOk(() => this.handleOnOk());
this._advancedDialog.render();
}
return this._advancedDialog;
}
public set advancedDialog(dialog: OptionsDialog) {
this._advancedDialog = dialog;
}
public static connectionOptionToServiceOption(connectionOption: azdata.ConnectionOption): azdata.ServiceOption {
return {
name: connectionOption.name,
displayName: connectionOption.displayName,
description: connectionOption.description,
groupName: connectionOption.groupName,
valueType: connectionOption.valueType,
defaultValue: connectionOption.defaultValue,
objectType: undefined,
categoryValues: connectionOption.categoryValues,
isRequired: connectionOption.isRequired,
isArray: undefined,
};
}
}

View File

@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { AddServerGroupAction, AddServerAction } from 'sql/workbench/contrib/objectExplorer/browser/connectionTreeAction';
import { ClearRecentConnectionsAction, GetCurrentConnectionStringAction } from 'sql/workbench/contrib/connection/browser/connectionActions';
import * as azdata from 'azdata';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { localize } from 'vs/nls';
import { ConnectionStatusbarItem } from 'sql/workbench/contrib/connection/browser/connectionStatus';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { integrated, azureMFA } from 'sql/platform/connection/common/constants';
import { AuthenticationType } from 'sql/workbench/services/connection/browser/connectionWidget';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(ConnectionStatusbarItem, LifecyclePhase.Restored);
// Connection Dashboard registration
const actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
// Connection Actions
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
ClearRecentConnectionsAction,
ClearRecentConnectionsAction.ID,
ClearRecentConnectionsAction.LABEL
),
ClearRecentConnectionsAction.LABEL
);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
AddServerGroupAction,
AddServerGroupAction.ID,
AddServerGroupAction.LABEL
),
AddServerGroupAction.LABEL
);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
AddServerAction,
AddServerAction.ID,
AddServerAction.LABEL
),
AddServerAction.LABEL
);
CommandsRegistry.registerCommand('azdata.connect',
function (accessor, args: {
serverName: string,
providerName: string,
authenticationType?: AuthenticationType,
userName?: string,
password?: string,
databaseName?: string
}) {
const capabilitiesServices = accessor.get(ICapabilitiesService);
const connectionManagementService = accessor.get(IConnectionManagementService);
if (args && args.serverName && args.providerName
&& (args.authenticationType === integrated
|| args.authenticationType === azureMFA
|| (args.userName && args.password))) {
const profile: azdata.IConnectionProfile = {
serverName: args.serverName,
databaseName: args.databaseName,
authenticationType: args.authenticationType,
providerName: args.providerName,
connectionName: '',
userName: args.userName,
password: args.password,
savePassword: true,
groupFullName: undefined,
saveProfile: true,
id: undefined,
groupId: undefined,
options: {}
};
const connectionProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesServices, profile);
connectionManagementService.connect(connectionProfile, undefined, {
saveTheConnection: true,
showDashboard: true,
params: undefined,
showConnectionDialogOnError: true,
showFirewallRuleOnError: true
});
} else {
connectionManagementService.showConnectionDialog();
}
});
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
GetCurrentConnectionStringAction,
GetCurrentConnectionStringAction.ID,
GetCurrentConnectionStringAction.LABEL
),
GetCurrentConnectionStringAction.LABEL
);
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'connection',
'title': 'Connection',
'type': 'object',
'properties': {
'sql.maxRecentConnections': {
'type': 'number',
'default': 25,
'description': localize('sql.maxRecentConnectionsDescription', "The maximum number of recently used connections to store in the connection list.")
},
'sql.defaultEngine': {
'type': 'string',
'description': localize('sql.defaultEngineDescription', "Default SQL Engine to use. This drives default language provider in .sql files and the default to use when creating a new connection. Valid option is currently MSSQL"),
'default': 'MSSQL'
},
'connection.parseClipboardForConnectionString': {
'type': 'boolean',
'default': true,
'description': localize('connection.parseClipboardForConnectionStringDescription', "Attempt to parse the contents of the clipboard when the connection dialog is opened or a paste is performed.")
}
}
});

View File

@@ -0,0 +1,179 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { Event, Emitter } from 'vs/base/common/event';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { INotificationService, INotificationActions } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { QueryInput } from 'sql/workbench/contrib/query/common/queryInput';
import { EditDataInput } from 'sql/workbench/contrib/editData/browser/editDataInput';
import { DashboardInput } from 'sql/workbench/contrib/dashboard/browser/dashboardInput';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { find } from 'vs/base/common/arrays';
/**
* Workbench action to clear the recent connnections list
*/
export class ClearRecentConnectionsAction extends Action {
public static ID = 'clearRecentConnectionsAction';
public static LABEL = nls.localize('ClearRecentlyUsedLabel', "Clear List");
public static ICON = 'search-action clear-search-results';
private _onRecentConnectionsRemoved = new Emitter<void>();
public onRecentConnectionsRemoved: Event<void> = this._onRecentConnectionsRemoved.event;
private _useConfirmationMessage = false;
constructor(
id: string,
label: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@INotificationService private _notificationService: INotificationService,
@IQuickInputService private _quickInputService: IQuickInputService,
@IDialogService private _dialogService: IDialogService,
) {
super(id, label, ClearRecentConnectionsAction.ICON);
this.enabled = true;
}
public set useConfirmationMessage(value: boolean) {
this._useConfirmationMessage = value;
}
public run(): Promise<void> {
if (this._useConfirmationMessage) {
return this.promptConfirmationMessage().then(result => {
if (result.confirmed) {
this._connectionManagementService.clearRecentConnectionsList();
this._onRecentConnectionsRemoved.fire();
}
});
} else {
return this.promptQuickOpenService().then(result => {
if (result) {
this._connectionManagementService.clearRecentConnectionsList();
const actions: INotificationActions = { primary: [] };
this._notificationService.notify({
severity: Severity.Info,
message: nls.localize('ClearedRecentConnections', "Recent connections list cleared"),
actions
});
this._onRecentConnectionsRemoved.fire();
}
});
}
}
private promptQuickOpenService(): Promise<boolean> {
const self = this;
return new Promise<boolean>((resolve, reject) => {
let choices: { key, value }[] = [
{ key: nls.localize('connectionAction.yes', "Yes"), value: true },
{ key: nls.localize('connectionAction.no', "No"), value: false }
];
self._quickInputService.pick(choices.map(x => x.key), { placeHolder: nls.localize('ClearRecentlyUsedLabel', "Clear List"), ignoreFocusLost: true }).then((choice) => {
let confirm = find(choices, x => x.key === choice);
resolve(confirm && confirm.value);
});
});
}
private promptConfirmationMessage(): Promise<IConfirmationResult> {
let confirm: IConfirmation = {
message: nls.localize('clearRecentConnectionMessage', "Are you sure you want to delete all the connections from the list?"),
primaryButton: nls.localize('connectionDialog.yes', "Yes"),
secondaryButton: nls.localize('connectionDialog.no', "No"),
type: 'question'
};
return new Promise<IConfirmationResult>((resolve, reject) => {
this._dialogService.confirm(confirm).then((confirmed) => {
resolve(confirmed);
});
});
}
}
/**
* Action to delete one recently used connection from the MRU
*/
export class ClearSingleRecentConnectionAction extends Action {
public static ID = 'clearSingleRecentConnectionAction';
public static LABEL = nls.localize('delete', "Delete");
private _onRecentConnectionRemoved = new Emitter<void>();
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
constructor(
id: string,
label: string,
private _connectionProfile: IConnectionProfile,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
) {
super(id, label);
this.enabled = true;
}
public run(): Promise<void> {
return new Promise<void>((resolve, reject) => {
resolve(this._connectionManagementService.clearRecentConnection(this._connectionProfile));
this._onRecentConnectionRemoved.fire();
});
}
}
/**
* Action to retrieve the current connection string
*/
export class GetCurrentConnectionStringAction extends Action {
public static ID = 'getCurrentConnectionStringAction';
public static LABEL = nls.localize('connectionAction.GetCurrentConnectionString', "Get Current Connection String");
constructor(
id: string,
label: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IEditorService private _editorService: IEditorService,
@INotificationService private readonly _notificationService: INotificationService,
@IClipboardService private _clipboardService: IClipboardService,
) {
super(GetCurrentConnectionStringAction.ID, GetCurrentConnectionStringAction.LABEL);
this.enabled = true;
}
public run(): Promise<void> {
return new Promise<void>((resolve, reject) => {
let activeInput = this._editorService.activeEditor;
if (activeInput && (activeInput instanceof QueryInput || activeInput instanceof EditDataInput || activeInput instanceof DashboardInput)
&& this._connectionManagementService.isConnected(activeInput.uri)) {
let includePassword = false;
let connectionProfile = this._connectionManagementService.getConnectionProfile(activeInput.uri);
this._connectionManagementService.getConnectionString(connectionProfile.id, includePassword).then(result => {
//Copy to clipboard
this._clipboardService.writeText(result);
let message = result
? result
: nls.localize('connectionAction.connectionString', "Connection string not available");
this._notificationService.info(message);
});
} else {
let message = nls.localize('connectionAction.noConnection', "No active connection available");
this._notificationService.info(message);
}
});
}
}

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
import * as TaskUtilities from 'sql/workbench/browser/taskUtilities';
import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { localize } from 'vs/nls';
// Connection status bar showing the current global connection
export class ConnectionStatusbarItem extends Disposable implements IWorkbenchContribution {
private static readonly ID = 'status.connection.status';
private statusItem: IStatusbarEntryAccessor;
constructor(
@IStatusbarService private readonly statusbarService: IStatusbarService,
@IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService,
@IEditorService private readonly editorService: IEditorService,
@IObjectExplorerService private readonly objectExplorerService: IObjectExplorerService,
) {
super();
this.statusItem = this._register(
this.statusbarService.addEntry({
text: '',
},
ConnectionStatusbarItem.ID,
localize('status.connection.status', "Connection Status"),
StatusbarAlignment.RIGHT, 100)
);
this.hide();
this._register(this.connectionManagementService.onConnect(() => this._updateStatus()));
this._register(this.connectionManagementService.onConnectionChanged(() => this._updateStatus()));
this._register(this.connectionManagementService.onDisconnect(() => this._updateStatus()));
this._register(this.editorService.onDidActiveEditorChange(() => this._updateStatus()));
this._register(this.objectExplorerService.onSelectionOrFocusChange(() => this._updateStatus()));
}
private hide() {
this.statusbarService.updateEntryVisibility(ConnectionStatusbarItem.ID, false);
}
private show() {
this.statusbarService.updateEntryVisibility(ConnectionStatusbarItem.ID, true);
}
// Update the connection status shown in the bar
private _updateStatus(): void {
let activeConnection = TaskUtilities.getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService);
if (activeConnection) {
this._setConnectionText(activeConnection);
this.show();
} else {
this.hide();
}
}
// Set connection info to connection status bar
private _setConnectionText(connectionProfile: IConnectionProfile): void {
let text: string = connectionProfile.serverName;
if (text) {
if (connectionProfile.databaseName && connectionProfile.databaseName !== '') {
text = text + ' : ' + connectionProfile.databaseName;
} else {
text = text + ' : ' + '<default>';
}
}
let tooltip: string =
'Server: ' + connectionProfile.serverName + '\r\n' +
'Database: ' + (connectionProfile.databaseName ? connectionProfile.databaseName : '<default>') + '\r\n';
if (connectionProfile.userName && connectionProfile.userName !== '') {
tooltip = tooltip + 'Login: ' + connectionProfile.userName + '\r\n';
}
this.statusItem.update({
text, tooltip
});
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>

After

Width:  |  Height:  |  Size: 139 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:#212121;}.cls-2{fill:#3bb44a;}</style></defs><title>connected_active_server_16x16</title><path class="cls-1" d="M1.29.07V16h9.59a3.31,3.31,0,0,1-1.94-1H2.29V11h6a3.31,3.31,0,0,1,.54-.8A1.81,1.81,0,0,1,9,10H2.29v-9h8.53v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.3,1.8V4.25H5.75V1.8Zm2,2H3.8V2.3H5.25Z"/><circle class="cls-2" cx="11.24" cy="12.52" r="3.47"/></svg>

After

Width:  |  Height:  |  Size: 502 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;}.cls-2{fill:#3bb44a;}</style></defs><title>connected_active_server_inverse_16x16</title><path class="cls-1" d="M1.29.07V16h9.59a3.31,3.31,0,0,1-1.94-1H2.29V11h6a3.31,3.31,0,0,1,.54-.8A1.81,1.81,0,0,1,9,10H2.29v-9h8.53v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.3,1.8V4.25H5.75V1.8Zm2,2H3.8V2.3H5.25Z"/><circle class="cls-2" cx="11.24" cy="12.52" r="3.47"/></svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --- Registered servers tree viewlet --- */
.server-explorer-viewlet .monaco-tree .monaco-tree-row .content .server-group {
cursor: default;
width: 100%;
display: flex;
align-items: center;
}
/* Bold font style does not go well with CJK fonts */
.server-explorer-viewlet:lang(zh-Hans) .monaco-tree .monaco-tree-row .server-group,
.server-explorer-viewlet:lang(zh-Hant) .monaco-tree .monaco-tree-row .server-group,
.server-explorer-viewlet:lang(ja) .monaco-tree .monaco-tree-row .server-group,
.server-explorer-viewlet:lang(ko) .monaco-tree .monaco-tree-row .server-group { font-weight: normal; }
/* High Contrast Theming */
.hc-black .monaco-workbench .server-explorer-viewlet .server-group {
line-height: 20px;
}
.monaco-workbench > .activitybar .monaco-action-bar .action-label.serverTree {
background-size: 22px;
background-repeat: no-repeat;
background-position: 50% !important;
}
.server-explorer-viewlet .object-explorer-view {
height: calc(100% - 36px);
}
.server-explorer-viewlet .server-group {
height: 38px;
line-height: 38px;
color: #ffffff;
}
.server-explorer-viewlet .monaco-action-bar .action-label {
margin-right: 0.3em;
margin-left: 0.3em;
line-height: 15px;
width: 10px !important;
height: 10px !important;
}
/* Add space beneath the button */
.new-connection .monaco-text-button {
margin-bottom: 2px;
}
/* display action buttons on hover */
.server-explorer-viewlet .monaco-tree .monaco-tree-row > .content {
display: flex;
}
/* Added to display the tree in connection dialog */
.server-explorer-viewlet {
height: 100%;
}
.explorer-servers {
height: 100%;
}
/* search box */
.server-explorer-viewlet .search-box {
padding-bottom: 4px;
margin: auto;
width: 95%;
}
/* OE and connection element group */
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile,
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group {
padding: 5px;
overflow: hidden;
}
/* OE and connection label */
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .label,
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .label {
text-overflow: ellipsis;
overflow: hidden;
}
/* OE and connection icon */
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon,
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .icon {
float: left;
height: 16px;
width: 16px;
padding-right: 10px;
}
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected {
background: url('connected_active_server.svg') center center no-repeat;
}
.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected,
.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected{
background: url('connected_active_server_inverse.svg') center center no-repeat;
}
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected {
background: url('disconnected_server.svg') center center no-repeat;
}
.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected,
.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected{
background: url('disconnected_server_inverse.svg') center center no-repeat;
}
/* loading for OE node */
.server-explorer-viewlet .monaco-tree .monaco-tree-rows > .monaco-tree-row > .codicon.in-progress .connection-tile:before,
.server-explorer-viewlet .monaco-tree .monaco-tree-rows > .monaco-tree-row > .codicon.in-progress .object-element-group:before {
position: absolute;
display: block;
width: 36px;
height: 100%;
top: 0;
left: -35px;
}
.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded.has-children > .content.server-group:before {
background: url('expanded-dark.svg') 50% 50% no-repeat;
}
.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content.server-group:before {
background: url('collapsed-dark.svg') 50% 50% no-repeat;
}
/* Add connection button */
.server-explorer-viewlet .button-section {
padding: 20px;
}

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:#212121;}.cls-2{fill:#d02e00;}</style></defs><title>disconnected_server_16x16</title><path class="cls-1" d="M1.42.06V16H11a3.31,3.31,0,0,1-1.94-1H2.42V11h6a3.31,3.31,0,0,1,.54-.8,1.81,1.81,0,0,1,.19-.2H2.42v-9H11v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.43,1.79V4.24H5.89V1.79Zm2,2H3.93V2.29H5.39Z"/><path class="cls-2" d="M11.08,16a2.22,2.22,0,0,0,.45,0,2.59,2.59,0,0,0,.4,0Z"/><path class="cls-2" d="M12,9.08h0l-.39,0a3.68,3.68,0,0,0-.58,0A3.41,3.41,0,0,0,9.14,10a1.81,1.81,0,0,0-.19.2,3.46,3.46,0,0,0-.89,2.3,3.4,3.4,0,0,0,.85,2.26,1.29,1.29,0,0,0,.16.17A3.31,3.31,0,0,0,11,16H12a3.46,3.46,0,0,0,2.17-1.13,3.41,3.41,0,0,0,.88-2.3A3.47,3.47,0,0,0,12,9.08Zm0,5.72a1.72,1.72,0,0,1-.39,0,2.23,2.23,0,0,1-.77-.14,2.29,2.29,0,0,1-1.54-2.17A2.22,2.22,0,0,1,9.79,11a2.29,2.29,0,0,1,1-.67,2.23,2.23,0,0,1,.77-.14,1.72,1.72,0,0,1,.39,0h0a2.3,2.3,0,0,1,0,4.53Z"/></svg>

After

Width:  |  Height:  |  Size: 1002 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;}.cls-2{fill:#d02e00;}</style></defs><title>disconnected_server_inverse_16x16</title><path class="cls-1" d="M1.42.06V16H11a3.31,3.31,0,0,1-1.94-1H2.42V11h6a3.31,3.31,0,0,1,.54-.8,1.81,1.81,0,0,1,.19-.2H2.42v-9H11v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.43,1.79V4.24H5.89V1.79Zm2,2H3.93V2.29H5.39Z"/><path class="cls-2" d="M11.08,16a2.22,2.22,0,0,0,.45,0,2.59,2.59,0,0,0,.4,0Z"/><path class="cls-2" d="M12,9.08h0l-.39,0a3.68,3.68,0,0,0-.58,0A3.41,3.41,0,0,0,9.14,10a1.81,1.81,0,0,0-.19.2,3.46,3.46,0,0,0-.89,2.3,3.4,3.4,0,0,0,.85,2.26,1.29,1.29,0,0,0,.16.17A3.31,3.31,0,0,0,11,16H12a3.46,3.46,0,0,0,2.17-1.13,3.41,3.41,0,0,0,.88-2.3A3.47,3.47,0,0,0,12,9.08Zm0,5.72a1.72,1.72,0,0,1-.39,0,2.23,2.23,0,0,1-.77-.14,2.29,2.29,0,0,1-1.54-2.17A2.22,2.22,0,0,1,9.79,11a2.29,2.29,0,0,1,1-.67,2.23,2.23,0,0,1,.77-.14,1.72,1.72,0,0,1,.39,0h0a2.3,2.3,0,0,1,0,4.53Z"/></svg>

After

Width:  |  Height:  |  Size: 1007 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M11 10H5.344L11 4.414V10z"/></svg>

After

Width:  |  Height:  |  Size: 118 B

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ClearSingleRecentConnectionAction } from 'sql/workbench/contrib/connection/browser/connectionActions';
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IAction } from 'vs/base/common/actions';
import { Event, Emitter } from 'vs/base/common/event';
import mouse = require('vs/base/browser/mouseEvent');
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
export class RecentConnectionActionsProvider extends ContributableActionProvider {
private _onRecentConnectionRemoved = new Emitter<void>();
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService
) {
super();
}
private getRecentConnectionActions(tree: ITree, element: any): IAction[] {
let actions: IAction[] = [];
let clearSingleConnectionAction = this._instantiationService.createInstance(ClearSingleRecentConnectionAction, ClearSingleRecentConnectionAction.ID,
ClearSingleRecentConnectionAction.LABEL, <IConnectionProfile>element);
clearSingleConnectionAction.onRecentConnectionRemoved(() => this._onRecentConnectionRemoved.fire());
actions.push(clearSingleConnectionAction);
return actions;
}
public hasActions(tree: ITree, element: any): boolean {
return element instanceof ConnectionProfile;
}
/**
* Return actions given an element in the tree
*/
public getActions(tree: ITree, element: any): IAction[] {
if (element instanceof ConnectionProfile) {
return this.getRecentConnectionActions(tree, element);
}
return [];
}
}
export class RecentConnectionsActionsContext {
public connectionProfile: ConnectionProfile;
public container: HTMLElement;
public tree: ITree;
}
export class RecentConnectionTreeController extends DefaultController {
private _onRecentConnectionRemoved = new Emitter<void>();
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
constructor(
private clickcb: (element: any, eventish: ICancelableEvent, origin: string) => void,
private actionProvider: RecentConnectionActionsProvider,
private _connectionManagementService: IConnectionManagementService,
@IContextMenuService private _contextMenuService: IContextMenuService
) {
super();
}
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
this.clickcb(element, eventish, origin);
return super.onLeftClick(tree, element, eventish, origin);
}
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
super.onEnter(tree, event);
this.clickcb(tree.getSelection()[0], event, 'keyboard');
return true;
}
protected onRightClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
this.clickcb(element, eventish, origin);
this.showContextMenu(tree, element, eventish);
return true;
}
public onMouseDown(tree: ITree, element: any, event: mouse.IMouseEvent, origin: string = 'mouse'): boolean {
if (event.leftButton || event.middleButton) {
return this.onLeftClick(tree, element, event, origin);
} else {
return this.onRightClick(tree, element, event);
}
}
public onKeyDown(tree: ITree, event: IKeyboardEvent): boolean {
if (event.keyCode === 20) {
let element = tree.getFocus();
if (element instanceof ConnectionProfile) {
this._connectionManagementService.clearRecentConnection(element);
this._onRecentConnectionRemoved.fire();
return true;
}
}
return super.onKeyDown(tree, event);
}
public showContextMenu(tree: ITree, element: any, event: any): boolean {
let actionContext: any;
if (element instanceof ConnectionProfile) {
actionContext = new RecentConnectionsActionsContext();
actionContext.container = event.target;
actionContext.connectionProfile = <ConnectionProfile>element;
actionContext.tree = tree;
} else {
actionContext = element;
}
let anchor = { x: event.x + 1, y: event.y };
this._contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => this.actionProvider.getActions(tree, element),
onHide: (wasCancelled?: boolean) => {
if (wasCancelled) {
tree.domFocus();
}
},
getActionsContext: () => (actionContext)
});
return true;
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export class SavedConnectionTreeController extends DefaultController {
constructor(private clickcb: (element: any, eventish: ICancelableEvent, origin: string) => void) {
super();
}
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
this.clickcb(element, eventish, origin);
return super.onLeftClick(tree, element, eventish, origin);
}
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
super.onEnter(tree, event);
// grab the current selection for use later
let selection = tree.getSelection();
this.clickcb(selection[0], event, 'keyboard');
tree.toggleExpansion(selection[0]);
return true;
}
}

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// ------------------------------- < Cancel Connect Request > ---------------------------------------
/**
* Cancel connect request message format
*/
export class CancelConnectParams {
/**
* URI identifying the owner of the connection
*/
public ownerUri: string;
}
// ------------------------------- </ Cancel Connect Request > --------------------------------------
// ------------------------------- < Disconnect Request > -------------------------------------------
// Disconnect request message format
export class DisconnectParams {
// URI identifying the owner of the connection
public ownerUri: string;
}
// ------------------------------- </ Disconnect Request > ------------------------------------------

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IConnectionProfile } from 'azdata';
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
export class ConnectionContextKey implements IContextKey<IConnectionProfile> {
static Provider = new RawContextKey<string>('connectionProvider', undefined);
static Server = new RawContextKey<string>('serverName', undefined);
static Database = new RawContextKey<string>('databaseName', undefined);
static Connection = new RawContextKey<IConnectionProfile>('connection', undefined);
static IsQueryProvider = new RawContextKey<boolean>('isQueryProvider', false);
private _providerKey: IContextKey<string>;
private _serverKey: IContextKey<string>;
private _databaseKey: IContextKey<string>;
private _connectionKey: IContextKey<IConnectionProfile>;
private _isQueryProviderKey: IContextKey<boolean>;
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IQueryManagementService private queryManagementService: IQueryManagementService
) {
this._providerKey = ConnectionContextKey.Provider.bindTo(contextKeyService);
this._serverKey = ConnectionContextKey.Server.bindTo(contextKeyService);
this._databaseKey = ConnectionContextKey.Database.bindTo(contextKeyService);
this._connectionKey = ConnectionContextKey.Connection.bindTo(contextKeyService);
this._isQueryProviderKey = ConnectionContextKey.IsQueryProvider.bindTo(contextKeyService);
}
set(value: IConnectionProfile) {
let queryProviders = this.queryManagementService.getRegisteredProviders();
this._connectionKey.set(value);
this._providerKey.set(value && value.providerName);
this._serverKey.set(value && value.serverName);
this._databaseKey.set(value && value.databaseName);
this._isQueryProviderKey.set(value && value.providerName && queryProviders.indexOf(value.providerName) !== -1);
}
reset(): void {
this._providerKey.reset();
this._serverKey.reset();
this._databaseKey.reset();
this._connectionKey.reset();
this._isQueryProviderKey.reset();
}
public get(): IConnectionProfile {
return this._connectionKey.get();
}
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ConnectionSummary } from 'azdata';
import * as LocalizedConstants from 'sql/workbench/contrib/connection/common/localizedConstants';
import { INotificationService } from 'vs/platform/notification/common/notification';
// Status when making connections from the viewlet
export class ConnectionGlobalStatus {
private _displayTime: number = 5000; // (in ms)
constructor(
@INotificationService private _notificationService: INotificationService
) {
}
public setStatusToConnected(connectionSummary: ConnectionSummary): void {
if (this._notificationService) {
let text: string;
let connInfo: string = connectionSummary.serverName;
if (connInfo) {
if (connectionSummary.databaseName && connectionSummary.databaseName !== '') {
connInfo = connInfo + ' : ' + connectionSummary.databaseName;
} else {
connInfo = connInfo + ' : ' + '<default>';
}
text = LocalizedConstants.onDidConnectMessage + ' ' + connInfo;
}
this._notificationService.status(text, { hideAfter: this._displayTime });
}
}
public setStatusToDisconnected(fileUri: string): void {
if (this._notificationService) {
this._notificationService.status(LocalizedConstants.onDidDisconnectMessage, { hideAfter: this._displayTime });
}
}
}

View File

@@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { deepClone } from 'vs/base/common/objects';
import * as resources from 'vs/base/common/resources';
import { ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService';
export const Extensions = {
ConnectionProviderContributions: 'connection.providers'
};
export interface IConnectionProviderRegistry {
registerConnectionProvider(id: string, properties: ConnectionProviderProperties): void;
getProperties(id: string): ConnectionProviderProperties | undefined;
readonly onNewProvider: Event<{ id: string, properties: ConnectionProviderProperties }>;
readonly providers: { [id: string]: ConnectionProviderProperties };
}
class ConnectionProviderRegistryImpl implements IConnectionProviderRegistry {
private _providers = new Map<string, ConnectionProviderProperties>();
private _onNewProvider = new Emitter<{ id: string, properties: ConnectionProviderProperties }>();
public readonly onNewProvider: Event<{ id: string, properties: ConnectionProviderProperties }> = this._onNewProvider.event;
public registerConnectionProvider(id: string, properties: ConnectionProviderProperties): void {
this._providers.set(id, properties);
this._onNewProvider.fire({ id, properties });
}
public getProperties(id: string): ConnectionProviderProperties | undefined {
return this._providers.get(id);
}
public get providers(): { [id: string]: ConnectionProviderProperties } {
let rt: { [id: string]: ConnectionProviderProperties } = {};
this._providers.forEach((v, k) => {
rt[k] = deepClone(v);
});
return rt;
}
}
const connectionRegistry = new ConnectionProviderRegistryImpl();
Registry.add(Extensions.ConnectionProviderContributions, connectionRegistry);
const ConnectionProviderContrib: IJSONSchema = {
type: 'object',
properties: {
providerId: {
type: 'string',
description: localize('schema.providerId', "Common id for the provider")
},
displayName: {
type: 'string',
description: localize('schema.displayName', "Display Name for the provider")
},
iconPath: {
description: localize('schema.iconPath', "Icon path for the server type"),
oneOf: [
{
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
},
path: {
type: 'object',
properties: {
light: {
type: 'string',
},
dark: {
type: 'string',
}
}
}
}
}
},
{
type: 'object',
properties: {
light: {
type: 'string',
},
dark: {
type: 'string',
}
}
},
{
type: 'string'
}
]
},
connectionOptions: {
type: 'array',
description: localize('schema.connectionOptions', "Options for connection"),
items: {
type: 'object',
properties: {
specialValueType: {
type: 'string'
},
isIdentity: {
type: 'boolean'
},
name: {
type: 'string'
},
displayName: {
type: 'string'
},
description: {
type: 'string'
},
groupName: {
type: 'string'
},
valueType: {
type: 'string'
},
defaultValue: {
type: 'any'
},
objectType: {
type: 'any'
},
categoryValues: {
type: 'any'
},
isRequired: {
type: 'boolean'
},
isArray: {
type: 'bolean'
}
}
}
}
},
required: ['providerId']
};
ExtensionsRegistry.registerExtensionPoint<ConnectionProviderProperties | ConnectionProviderProperties[]>({ extensionPoint: 'connectionProvider', jsonSchema: ConnectionProviderContrib }).setHandler(extensions => {
function handleCommand(contrib: ConnectionProviderProperties, extension: IExtensionPointUser<any>) {
connectionRegistry.registerConnectionProvider(contrib.providerId, contrib);
}
for (let extension of extensions) {
const { value } = extension;
resolveIconPath(extension);
if (Array.isArray<ConnectionProviderProperties>(value)) {
for (let command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
});
function resolveIconPath(extension: IExtensionPointUser<any>): void {
if (!extension || !extension.value) { return undefined; }
let toAbsolutePath = (iconPath: any) => {
if (!iconPath || !baseDir) { return; }
if (Array.isArray(iconPath)) {
for (let e of iconPath) {
e.path = {
light: resources.joinPath(extension.description.extensionLocation, e.path.light.toString()),
dark: resources.joinPath(extension.description.extensionLocation, e.path.dark.toString())
};
}
} else if (typeof iconPath === 'string') {
iconPath = {
light: resources.joinPath(extension.description.extensionLocation, iconPath),
dark: resources.joinPath(extension.description.extensionLocation, iconPath)
};
} else {
iconPath = {
light: resources.joinPath(extension.description.extensionLocation, iconPath.light.toString()),
dark: resources.joinPath(extension.description.extensionLocation, iconPath.dark.toString())
};
}
};
let baseDir = extension.description.extensionLocation.fsPath;
let properties: ConnectionProviderProperties = extension.value;
if (Array.isArray<ConnectionProviderProperties>(properties)) {
for (let p of properties) {
toAbsolutePath(p['iconPath']);
}
} else {
toAbsolutePath(properties['iconPath']);
}
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
export const onDidConnectMessage = localize('onDidConnectMessage', "Connected to");
export const onDidDisconnectMessage = localize('onDidDisconnectMessage', "Disconnected");
export const unsavedGroupLabel = localize('unsavedGroupLabel', "Unsaved Connections");

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ServerInfo } from 'azdata';
import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes';
export class ServerInfoContextKey implements IContextKey<ServerInfo> {
static ServerInfo = new RawContextKey<ServerInfo>('serverInfo', undefined);
static ServerMajorVersion = new RawContextKey<string>('serverMajorVersion', undefined);
static IsCloud = new RawContextKey<boolean>('isCloud', undefined);
static IsBigDataCluster = new RawContextKey<boolean>('isBigDataCluster', undefined);
static EngineEdition = new RawContextKey<number>('engineEdition', undefined);
private _serverInfo: IContextKey<ServerInfo>;
private _serverMajorVersion: IContextKey<string>;
private _isCloud: IContextKey<boolean>;
private _isBigDataCluster: IContextKey<boolean>;
private _engineEdition: IContextKey<number>;
constructor(
@IContextKeyService contextKeyService: IContextKeyService
) {
this._serverInfo = ServerInfoContextKey.ServerInfo.bindTo(contextKeyService);
this._serverMajorVersion = ServerInfoContextKey.ServerMajorVersion.bindTo(contextKeyService);
this._isCloud = ServerInfoContextKey.IsCloud.bindTo(contextKeyService);
this._isBigDataCluster = ServerInfoContextKey.IsBigDataCluster.bindTo(contextKeyService);
this._engineEdition = ServerInfoContextKey.EngineEdition.bindTo(contextKeyService);
}
set(value: ServerInfo) {
this._serverInfo.set(value);
let majorVersion = value && value.serverMajorVersion;
this._serverMajorVersion.set(majorVersion && `${majorVersion}`);
this._isCloud.set(value && value.isCloud);
this._isBigDataCluster.set(value && value.options && value.options['isBigDataCluster']);
let engineEditionId = value && value.engineEditionId;
engineEditionId ? this._engineEdition.set(engineEditionId) : this._engineEdition.set(DatabaseEngineEdition.Unknown);
}
reset(): void {
this._serverMajorVersion.reset();
this._isCloud.reset();
this._isBigDataCluster.reset();
this._engineEdition.reset();
}
public get(): ServerInfo {
return this._serverInfo.get();
}
}

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.
*--------------------------------------------------------------------------------------------*/
import { OptionsDialog } from 'sql/workbench/browser/modal/optionsDialog';
import { AdvancedPropertiesController } from 'sql/workbench/contrib/connection/browser/advancedPropertiesController';
import * as azdata from 'azdata';
import * as TypeMoq from 'typemoq';
import * as assert from 'assert';
import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
suite('Advanced properties dialog tests', () => {
let advancedController: AdvancedPropertiesController;
let providerOptions: azdata.ConnectionOption[];
setup(() => {
advancedController = new AdvancedPropertiesController(() => { }, null);
providerOptions = [
{
name: 'a1',
displayName: undefined,
description: undefined,
groupName: 'a',
categoryValues: undefined,
defaultValue: undefined,
isIdentity: true,
isRequired: true,
specialValueType: null,
valueType: ServiceOptionType.string
},
{
name: 'b1',
displayName: undefined,
description: undefined,
groupName: 'b',
categoryValues: undefined,
defaultValue: undefined,
isIdentity: true,
isRequired: true,
specialValueType: null,
valueType: ServiceOptionType.string
},
{
name: 'noType',
displayName: undefined,
description: undefined,
groupName: undefined,
categoryValues: undefined,
defaultValue: undefined,
isIdentity: true,
isRequired: true,
specialValueType: null,
valueType: ServiceOptionType.string
},
{
name: 'a2',
displayName: undefined,
description: undefined,
groupName: 'a',
categoryValues: undefined,
defaultValue: undefined,
isIdentity: true,
isRequired: true,
specialValueType: null,
valueType: ServiceOptionType.string
},
{
name: 'b2',
displayName: undefined,
description: undefined,
groupName: 'b',
categoryValues: undefined,
defaultValue: undefined,
isIdentity: true,
isRequired: true,
specialValueType: null,
valueType: ServiceOptionType.string
}
];
});
test('advanced dialog should open when showDialog in advancedController get called', () => {
let isAdvancedDialogCalled = false;
let options: { [name: string]: any } = {};
let advanceDialog = TypeMoq.Mock.ofType(OptionsDialog, TypeMoq.MockBehavior.Strict,
'', // title
'', // name
{}, // options
undefined, // partsService
undefined, // themeService
undefined, // Context view service
undefined, // instantiation Service
undefined, // telemetry service
new MockContextKeyService() // contextkeyservice
);
advanceDialog.setup(x => x.open(TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback(() => {
isAdvancedDialogCalled = true;
});
advancedController.advancedDialog = advanceDialog.object;
advancedController.showDialog(providerOptions, options);
assert.equal(isAdvancedDialogCalled, true);
});
});

View File

@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { registerContainer, generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { NAV_SECTION, validateNavSectionContributionAndRegisterIcon } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardNavSection.contribution';
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.contribution';
import { WEBVIEW_CONTAINER } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWebviewContainer.contribution';
import { values } from 'vs/base/common/collections';
import { find } from 'vs/base/common/arrays';
import { NavSectionConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
const containerTypes = [
WIDGETS_CONTAINER,
GRID_CONTAINER,
WEBVIEW_CONTAINER,
NAV_SECTION
];
export type IUserFriendlyIcon = string | { light: string; dark: string; };
export interface IDashboardContainerContrib {
id: string;
container: Record<string, NavSectionConfig[]>;
}
const containerSchema: IJSONSchema = {
type: 'object',
properties: {
id: {
type: 'string',
description: localize('azdata.extension.contributes.dashboard.container.id', "Unique identifier for this container.")
},
container: {
description: localize('azdata.extension.contributes.dashboard.container.container', "The container that will be displayed in the tab."),
type: 'object',
properties: generateContainerTypeSchemaProperties()
}
}
};
const containerContributionSchema: IJSONSchema = {
description: localize('azdata.extension.contributes.containers', "Contributes a single or multiple dashboard containers for users to add to their dashboard."),
oneOf: [
containerSchema,
{
type: 'array',
items: containerSchema
}
]
};
ExtensionsRegistry.registerExtensionPoint<IDashboardContainerContrib | IDashboardContainerContrib[]>({ extensionPoint: 'dashboard.containers', jsonSchema: containerContributionSchema }).setHandler(extensions => {
function handleCommand(dashboardContainer: IDashboardContainerContrib, extension: IExtensionPointUser<any>) {
const { id, container } = dashboardContainer;
if (!id) {
extension.collector.error(localize('dashboardContainer.contribution.noIdError', "No id in dashboard container specified for extension."));
return;
}
if (!container) {
extension.collector.error(localize('dashboardContainer.contribution.noContainerError', "No container in dashboard container specified for extension."));
return;
}
if (Object.keys(container).length !== 1) {
extension.collector.error(localize('dashboardContainer.contribution.moreThanOneDashboardContainersError', "Exactly 1 dashboard container must be defined per space."));
return;
}
let result = true;
const containerkey = Object.keys(container)[0];
const containerValue = values(container)[0];
const containerTypeFound = find(containerTypes, c => c === containerkey);
if (!containerTypeFound) {
extension.collector.error(localize('dashboardTab.contribution.unKnownContainerType', "Unknown container type defines in dashboard container for extension."));
return;
}
switch (containerkey) {
case WIDGETS_CONTAINER:
result = validateWidgetContainerContribution(extension, containerValue);
break;
case GRID_CONTAINER:
result = validateGridContainerContribution(extension, containerValue);
break;
case NAV_SECTION:
result = validateNavSectionContributionAndRegisterIcon(extension, containerValue);
break;
}
if (result) {
registerContainer({ id, container });
}
}
for (const extension of extensions) {
const { value } = extension;
if (Array.isArray<IDashboardContainerContrib>(value)) {
for (const command of value) {
handleCommand(command, extension);
}
} else {
handleCommand(value, extension);
}
}
});

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardControlHostContainer';
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { ControlHostContent } from 'sql/workbench/contrib/dashboard/browser/contents/controlHostContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
@Component({
selector: 'dashboard-controlhost-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardControlHostContainer) }],
template: `
<controlhost-content [webviewId]="tab.id">
</controlhost-content>
`
})
export class DashboardControlHostContainer extends DashboardTab implements AfterContentInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(ControlHostContent) private _hostContent: ControlHostContent;
constructor() {
super();
}
ngAfterContentInit(): void {
this._register(this._hostContent.onResize(() => {
this._onResize.fire();
}));
const container = <any>this.tab.container;
if (container['controlhost-container'] && container['controlhost-container'].type) {
this._hostContent.setControlType(container['controlhost-container'].type);
}
}
public layout(): void {
this._hostContent.layout();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public refresh(): void {
this._hostContent.refresh();
}
}

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 { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const CONTROLHOST_CONTAINER = 'controlhost-container';
let webviewSchema: IJSONSchema = {
type: 'null',
description: nls.localize('dashboard.container.controlhost', "The controlhost that will be displayed in this tab."),
default: null
};
registerContainerType(CONTROLHOST_CONTAINER, webviewSchema);
registerNavSectionContainerType(CONTROLHOST_CONTAINER, webviewSchema);

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-controlhost-container {
height: 100%;
width : 100%;
display: block;
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardErrorContainer';
import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ChangeDetectorRef, AfterViewInit } from '@angular/core';
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Event, Emitter } from 'vs/base/common/event';
import * as nls from 'vs/nls';
@Component({
selector: 'dashboard-error-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardErrorContainer) }],
template: `
<div class="error-container">
<div class="codicon globalError">
</div>
<div class="error-message" #errorMessage>
</div>
</div>
`
})
export class DashboardErrorContainer extends DashboardTab implements AfterViewInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild('errorMessage', { read: ElementRef }) private _errorMessageContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
ngAfterViewInit() {
const errorMessage = this._errorMessageContainer.nativeElement as HTMLElement;
errorMessage.innerText = nls.localize('dashboardNavSection.loadTabError', "The \"{0}\" section has invalid content. Please contact extension owner.", this.tab.title);
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return false;
}
public layout() {
}
public refresh(): void {
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-error-container {
height: 100%;
width: 100%;
}
dashboard-error-container .error-container {
padding: 6px;
background: #D02E00;
color: white;
}
dashboard-error-container .error-container .icon.globalError {
height: 16px;
width: 16px;
float: left;
padding-right: 15px;
}

View File

@@ -0,0 +1,33 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="fullsize" style="display: flex; flex-direction: column">
<div scrollable [horizontalScroll]="ScrollbarVisibility.Auto" [verticalScroll]="ScrollbarVisibility.Auto">
<table class="grid-table">
<tr *ngFor="let row of rows" class="grid-table-row">
<ng-container *ngFor="let col of cols">
<ng-container *ngIf="getContent(row,col) !== undefined">
<td class="table-cell" [colSpan]=getColspan(row,col) [rowSpan]=getRowspan(row,col)
[width]="getWidgetWidth(row,col)" [height]="getWidgetHeight(row,col)">
<dashboard-widget-wrapper *ngIf="isWidget(row,col)" [_config]="getWidgetContent(row,col)"
style="position:absolute;" [style.width]="getWidgetWidth(row,col)"
[style.height]="getWidgetHeight(row,col)">
</dashboard-widget-wrapper>
<webview-content *ngIf="isWebview(row,col)" [webviewId]="getWebviewId(row,col)">
</webview-content>
<modelview-content *ngIf="isModelView(row,col)" [modelViewId]="getModelViewId(row,col)">
</modelview-content>
</td>
</ng-container>
</ng-container>
</tr>
</table>
</div>
</div>

View File

@@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardGridContainer';
import { Component, Inject, Input, forwardRef, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, ContentChild } from '@angular/core';
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
import { TabConfig, WidgetConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { DashboardWidgetWrapper } from 'sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.component';
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
import { WebviewContent } from 'sql/workbench/contrib/dashboard/browser/contents/webviewContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Event, Emitter } from 'vs/base/common/event';
import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums';
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
import { values } from 'vs/base/common/collections';
import { fill } from 'vs/base/common/arrays';
export interface GridCellConfig {
id?: string;
row?: number;
col?: number;
colspan?: string | number;
rowspan?: string | number;
}
export interface GridWidgetConfig extends GridCellConfig, WidgetConfig {
}
export interface GridWebviewConfig extends GridCellConfig {
webview: {
id?: string;
};
}
export interface GridModelViewConfig extends GridCellConfig {
widget: {
modelview: {
id?: string;
}
};
}
@Component({
selector: 'dashboard-grid-container',
templateUrl: decodeURI(require.toUrl('./dashboardGridContainer.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardGridContainer) }]
})
export class DashboardGridContainer extends DashboardTab implements OnDestroy {
@Input() private tab: TabConfig;
private _contents: GridCellConfig[];
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
private cellWidth: number = 270;
private cellHeight: number = 270;
public ScrollbarVisibility = ScrollbarVisibility;
protected SKELETON_WIDTH = 5;
protected rows: number[];
protected cols: number[];
protected getContent(row: number, col: number): GridCellConfig {
const widget = this._contents.filter(w => w.row === row && w.col === col);
return widget ? widget[0] : undefined;
}
protected getWidgetContent(row: number, col: number): GridWidgetConfig {
const content = this.getContent(row, col);
if (content) {
const widgetConfig = <GridWidgetConfig>content;
if (widgetConfig && widgetConfig.widget) {
return widgetConfig;
}
}
return undefined;
}
protected getWebviewContent(row: number, col: number): GridWebviewConfig {
const content = this.getContent(row, col);
if (content) {
const webviewConfig = <GridWebviewConfig>content;
if (webviewConfig && webviewConfig.webview) {
return webviewConfig;
}
}
return undefined;
}
protected getModelViewContent(row: number, col: number): GridModelViewConfig {
const content = this.getContent(row, col);
if (content) {
const modelviewConfig = <GridModelViewConfig>content;
if (modelviewConfig && modelviewConfig.widget.modelview) {
return modelviewConfig;
}
}
return undefined;
}
protected isWidget(row: number, col: number): boolean {
const widgetConfig = this.getWidgetContent(row, col);
return widgetConfig !== undefined;
}
protected isWebview(row: number, col: number): boolean {
const webview = this.getWebviewContent(row, col);
return webview !== undefined;
}
protected getWebviewId(row: number, col: number): string {
const widgetConfig = this.getWebviewContent(row, col);
if (widgetConfig && widgetConfig.webview) {
return widgetConfig.webview.id;
}
return undefined;
}
protected isModelView(row: number, col: number): boolean {
const modelView = this.getModelViewContent(row, col);
return modelView !== undefined;
}
protected getModelViewId(row: number, col: number): string {
const widgetConfig = this.getModelViewContent(row, col);
if (widgetConfig && widgetConfig.widget.modelview) {
return widgetConfig.widget.modelview.id;
}
return undefined;
}
protected getColspan(row: number, col: number): string {
const content = this.getContent(row, col);
let colspan: string = '1';
if (content && content.colspan) {
colspan = this.convertToNumber(content.colspan, this.cols.length).toString();
}
return colspan;
}
protected getRowspan(row: number, col: number): string {
const content = this.getContent(row, col);
if (content && (content.rowspan)) {
return this.convertToNumber(content.rowspan, this.rows.length).toString();
} else {
return '1';
}
}
protected getWidgetWidth(row: number, col: number): string {
const colspan = this.getColspan(row, col);
const columnCount = this.convertToNumber(colspan, this.cols.length);
return columnCount * this.cellWidth + 'px';
}
protected getWidgetHeight(row: number, col: number): string {
const rowspan = this.getRowspan(row, col);
const rowCount = this.convertToNumber(rowspan, this.rows.length);
return rowCount * this.cellHeight + 'px';
}
private convertToNumber(value: string | number, maxNumber: number): number {
if (!value) {
return 1;
}
if (value === '*') {
return maxNumber;
}
try {
return +value;
} catch {
return 1;
}
}
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
@ViewChildren(WebviewContent) private _webViews: QueryList<WebviewContent>;
@ContentChild(ScrollableDirective) private _scrollable: ScrollableDirective;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
protected init() {
}
ngOnInit() {
if (this.tab.container) {
this._contents = values(this.tab.container)[0];
this._contents.forEach(widget => {
if (!widget.row) {
widget.row = 0;
}
if (!widget.col) {
widget.col = 0;
}
if (!widget.colspan) {
widget.colspan = '1';
}
if (!widget.rowspan) {
widget.rowspan = '1';
}
});
this.rows = this.createIndexes(this._contents.map(w => w.row));
this.cols = this.createIndexes(this._contents.map(w => w.col));
}
}
private createIndexes(indexes: number[]) {
const max = Math.max(...indexes) + 1;
return fill(max, 0).map((x, i) => i);
}
ngOnDestroy() {
this.dispose();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public layout() {
if (this._widgets) {
this._widgets.forEach(item => {
item.layout();
});
}
if (this._webViews) {
this._webViews.forEach(item => {
item.layout();
});
}
if (this._scrollable) {
this._scrollable.layout();
}
}
public refresh(): void {
if (this._widgets) {
this._widgets.forEach(item => {
item.refresh();
});
}
}
public enableEdit(): void {
}
}

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { generateDashboardGridLayoutSchema } from 'sql/workbench/contrib/dashboard/browser/pages/dashboardPageContribution';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { find } from 'vs/base/common/arrays';
export const GRID_CONTAINER = 'grid-container';
let gridContainersSchema: IJSONSchema = {
type: 'array',
description: nls.localize('dashboard.container.gridtab.items', "The list of widgets or webviews that will be displayed in this tab."),
items: generateDashboardGridLayoutSchema(undefined, true)
};
registerContainerType(GRID_CONTAINER, gridContainersSchema);
registerNavSectionContainerType(GRID_CONTAINER, gridContainersSchema);
export function validateGridContainerContribution(extension: IExtensionPointUser<any>, gridConfigs: object[]): boolean {
let result = true;
gridConfigs.forEach(widgetConfig => {
const allKeys = Object.keys(widgetConfig);
const widgetOrWebviewKey = find(allKeys, key => key === 'widget' || key === 'webview');
if (!widgetOrWebviewKey) {
result = false;
extension.collector.error(nls.localize('gridContainer.invalidInputs', "widgets or webviews are expected inside widgets-container for extension."));
return;
}
});
return result;
}

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.
*--------------------------------------------------------------------------------------------*/
dashboard-tab {
height: auto;
width: auto;
}
.grid-table {
border-spacing: 5px;
}
.grid-table-row {
width: auto;
clear: both;
}
.table-cell {
vertical-align: top;
padding: 7px;
}
.fullsize {
height: 100%;
width: 100%;
}

View File

@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardHomeContainer';
import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild, ContentChild } from '@angular/core';
import { DashboardWidgetContainer } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.component';
import { WidgetConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { DashboardServiceInterface } from 'sql/workbench/contrib/dashboard/browser/services/dashboardServiceInterface.service';
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
import { AngularEventType, IAngularEventingService } from 'sql/platform/angularEventing/browser/angularEventingService';
import { DashboardWidgetWrapper } from 'sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.component';
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums';
@Component({
selector: 'dashboard-home-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardHomeContainer) }],
template: `
<div class="fullsize" style="display: flex; flex-direction: column">
<div scrollable [horizontalScroll]="ScrollbarVisibility.Hidden" [verticalScroll]="ScrollbarVisibility.Auto">
<dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties"
style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'">
</dashboard-widget-wrapper>
<widget-content style="flex: 1" [scrollContent]="false" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
</widget-content>
</div>
</div>
`
})
export class DashboardHomeContainer extends DashboardWidgetContainer {
@Input() private properties: WidgetConfig;
@ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper;
@ContentChild(ScrollableDirective) private _scrollable: ScrollableDirective;
public ScrollbarVisibility = ScrollbarVisibility;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: DashboardServiceInterface,
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
@Inject(IAngularEventingService) private angularEventingService: IAngularEventingService
) {
super(_cd);
}
ngAfterContentInit() {
const collapsedVal = this.dashboardService.getSettings<string>(`${this.properties.context}.properties`);
if (collapsedVal === 'collapsed') {
this._propertiesClass.collapsed = true;
}
this.angularEventingService.onAngularEvent(this.dashboardService.getUnderlyingUri(), event => {
if (event.event === AngularEventType.COLLAPSE_WIDGET && this._propertiesClass && event.payload === this._propertiesClass.guid) {
this._propertiesClass.collapsed = !this._propertiesClass.collapsed;
this._cd.detectChanges();
this._configurationService.updateValue(`dashboard.${this.properties.context}.properties`,
this._propertiesClass.collapsed ? 'collapsed' : true, ConfigurationTarget.USER);
}
});
}
public layout() {
super.layout();
if (this._scrollable) {
this._scrollable.layout();
}
}
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-home-tab {
height: 100%;
width: 100%;
display: block;
}

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { ModelViewContent } from 'sql/workbench/browser/modelComponents/modelViewContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
@Component({
selector: 'dashboard-modelview-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardModelViewContainer) }],
template: `
<modelview-content [modelViewId]="tab.id">
</modelview-content>
`
})
export class DashboardModelViewContainer extends DashboardTab implements AfterContentInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent;
constructor() {
super();
}
ngAfterContentInit(): void {
this._register(this._modelViewContent.onResize(() => {
this._onResize.fire();
}));
}
public layout(): void {
this._modelViewContent.layout();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public refresh(): void {
// no op
}
}

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 { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const MODELVIEW_CONTAINER = 'modelview-container';
let modelviewSchema: IJSONSchema = {
type: 'null',
description: nls.localize('dashboard.container.modelview', "The model-backed view that will be displayed in this tab."),
default: null
};
registerContainerType(MODELVIEW_CONTAINER, modelviewSchema);
registerNavSectionContainerType(MODELVIEW_CONTAINER, modelviewSchema);

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-modelview-container {
height: 100%;
width : 100%;
display: block;
}

View File

@@ -0,0 +1,23 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<panel [options]="panelOpt" style="flex: 1 1 auto;" class="dashboard-panel">
<tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize"
[identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions" [iconClass]="tab.iconClass">
<ng-template>
<dashboard-webview-container *ngIf="getContentType(tab) === 'webview-container'" [tab]="tab">
</dashboard-webview-container>
<dashboard-widget-container *ngIf="getContentType(tab) === 'widgets-container'" [tab]="tab">
</dashboard-widget-container>
<dashboard-modelview-container *ngIf="getContentType(tab) === 'modelview-container'" [tab]="tab">
</dashboard-modelview-container>
<dashboard-grid-container *ngIf="getContentType(tab) === 'grid-container'" [tab]="tab">
</dashboard-grid-container>
<dashboard-error-container *ngIf="getContentType(tab) === 'error-container'" [tab]="tab">
</dashboard-error-container>
</ng-template>
</tab>
</panel>

View File

@@ -0,0 +1,181 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardNavSection';
import { Component, Inject, Input, forwardRef, ViewChild, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, OnChanges, AfterContentInit } from '@angular/core';
import { CommonServiceInterface, SingleConnectionManagementService } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
import { WidgetConfig, TabConfig, NavSectionConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { DashboardTab, IConfigModifierCollection } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
import { WIDGETS_CONTAINER } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.contribution';
import * as dashboardHelper from 'sql/workbench/contrib/dashboard/browser/core/dashboardHelper';
import { Event, Emitter } from 'vs/base/common/event';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILogService } from 'vs/platform/log/common/log';
import { find } from 'vs/base/common/arrays';
import { values } from 'vs/base/common/collections';
@Component({
selector: 'dashboard-nav-section',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardNavSection) }],
templateUrl: decodeURI(require.toUrl('./dashboardNavSection.component.html'))
})
export class DashboardNavSection extends DashboardTab implements OnDestroy, OnChanges, AfterContentInit, IConfigModifierCollection {
@Input() private tab: TabConfig;
protected tabs: Array<TabConfig> = [];
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
// tslint:disable-next-line:no-unused-variable
private readonly panelOpt: IPanelOptions = {
layout: NavigationBarLayout.vertical
};
// a set of config modifiers
private readonly _configModifiers: Array<(item: Array<WidgetConfig>, collection: IConfigModifierCollection, context: string) => Array<WidgetConfig>> = [
dashboardHelper.removeEmpty,
dashboardHelper.initExtensionConfigs,
dashboardHelper.addProvider,
dashboardHelper.addEdition,
dashboardHelper.addContext,
dashboardHelper.filterConfigs
];
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>, originalConfig: Array<WidgetConfig>) => Array<WidgetConfig>> = [
dashboardHelper.validateGridConfig
];
@ViewChildren(TabChild) private _tabs: QueryList<DashboardTab>;
@ViewChild(PanelComponent) private _panel: PanelComponent;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef,
@Inject(ILogService) private logService: ILogService
) {
super();
}
ngOnChanges() {
this.tabs = [];
let navSectionContainers: NavSectionConfig[] = [];
if (this.tab.container) {
navSectionContainers = values(this.tab.container)[0];
let hasIcon = true;
navSectionContainers.forEach(navSection => {
if (!navSection.iconClass) {
hasIcon = false;
}
});
this.panelOpt.showIcon = hasIcon;
this.loadNewTabs(navSectionContainers);
}
}
ngAfterContentInit(): void {
if (this._tabs) {
this._tabs.forEach(tabContent => {
this._register(tabContent.onResize(() => {
this._onResize.fire();
}));
});
}
}
ngOnDestroy() {
this.dispose();
}
private loadNewTabs(dashboardTabs: NavSectionConfig[]) {
if (dashboardTabs && dashboardTabs.length > 0) {
dashboardTabs.map(v => {
const containerResult = dashboardHelper.getDashboardContainer(v.container, this.logService);
if (!containerResult.result) {
return { id: v.id, title: v.title, container: { 'error-container': undefined } };
}
const key = Object.keys(containerResult.container)[0];
if (key === WIDGETS_CONTAINER || key === GRID_CONTAINER) {
let configs = <WidgetConfig[]>values(containerResult.container)[0];
this._configModifiers.forEach(cb => {
configs = cb.apply(this, [configs, this, this.tab.context]);
});
this._gridModifiers.forEach(cb => {
configs = cb.apply(this, [configs]);
});
if (key === WIDGETS_CONTAINER) {
return { id: v.id, title: v.title, container: { 'widgets-container': configs }, iconClass: v.iconClass };
} else {
return { id: v.id, title: v.title, container: { 'grid-container': configs }, iconClass: v.iconClass };
}
}
return { id: v.id, title: v.title, container: containerResult.container, iconClass: v.iconClass };
}).map(v => {
const config = v as TabConfig;
config.context = this.tab.context;
config.editable = false;
config.canClose = false;
this.addNewTab(config);
return config;
});
}
}
private addNewTab(tab: TabConfig): void {
const existedTab = find(this.tabs, i => i.id === tab.id);
if (!existedTab) {
this.tabs.push(tab);
this._cd.detectChanges();
}
}
protected getContentType(tab: TabConfig): string {
return tab.container ? Object.keys(tab.container)[0] : '';
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public layout() {
const activeTabId = this._panel.getActiveTab;
const localtab = this._tabs.find(i => i.id === activeTabId);
this._cd.detectChanges();
localtab.layout();
}
public refresh(): void {
if (this._tabs) {
this._tabs.forEach(tabContent => {
tabContent.refresh();
});
}
}
public enableEdit(): void {
if (this._tabs) {
this._tabs.forEach(tabContent => {
tabContent.enableEdit();
});
}
}
public get connectionManagementService(): SingleConnectionManagementService {
return this.dashboardService.connectionManagementService;
}
public get contextKeyService(): IContextKeyService {
return this.dashboardService.scopedContextKeyService;
}
}

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 { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom';
import { IdGenerator } from 'vs/base/common/idGenerator';
import * as resources from 'vs/base/common/resources';
import { NavSectionConfig, IUserFriendlyIcon } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { registerContainerType, generateNavSectionContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.contribution';
import { values } from 'vs/base/common/collections';
export const NAV_SECTION = 'nav-section';
const navSectionContainerSchema: IJSONSchema = {
type: 'object',
properties: {
id: {
type: 'string',
description: nls.localize('dashboard.container.left-nav-bar.id', "Unique identifier for this nav section. Will be passed to the extension for any requests.")
},
icon: {
description: nls.localize('dashboard.container.left-nav-bar.icon', "(Optional) Icon which is used to represent this nav section in the UI. Either a file path or a themeable configuration"),
anyOf: [{
type: 'string'
},
{
type: 'object',
properties: {
light: {
description: nls.localize('dashboard.container.left-nav-bar.icon.light', "Icon path when a light theme is used"),
type: 'string'
},
dark: {
description: nls.localize('dashboard.container.left-nav-bar.icon.dark', "Icon path when a dark theme is used"),
type: 'string'
}
}
}]
},
title: {
type: 'string',
description: nls.localize('dashboard.container.left-nav-bar.title', "Title of the nav section to show the user.")
},
container: {
description: nls.localize('dashboard.container.left-nav-bar.container', "The container that will be displayed in this nav section."),
type: 'object',
properties: generateNavSectionContainerTypeSchemaProperties()
}
}
};
const NavSectionSchema: IJSONSchema = {
type: 'array',
description: nls.localize('dashboard.container.left-nav-bar', "The list of dashboard containers that will be displayed in this navigation section."),
items: navSectionContainerSchema
};
registerContainerType(NAV_SECTION, NavSectionSchema);
function isValidIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): boolean {
if (typeof icon === 'undefined') {
return false;
}
if (typeof icon === 'string') {
return true;
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
return true;
}
extension.collector.error(nls.localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
return false;
}
const ids = new IdGenerator('contrib-dashboardNavSection-icon-');
function createCSSRuleForIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): string {
let iconClass: string;
if (icon) {
iconClass = ids.nextId();
if (typeof icon === 'string') {
const path = resources.joinPath(extension.description.extensionLocation, icon);
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(path)}`);
} else {
const light = resources.joinPath(extension.description.extensionLocation, icon.light);
const dark = resources.joinPath(extension.description.extensionLocation, icon.dark);
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(light)}`);
createCSSRule(`.vs-dark .codicon.${iconClass}, .hc-black .codicon.${iconClass}`, `background-image: ${asCSSUrl(dark)}`);
}
}
return iconClass;
}
export function validateNavSectionContributionAndRegisterIcon(extension: IExtensionPointUser<any>, navSectionConfigs: NavSectionConfig[]): boolean {
let result = true;
navSectionConfigs.forEach(section => {
if (!section.title) {
result = false;
extension.collector.error(nls.localize('navSection.missingTitle.error', "No title in nav section specified for extension."));
}
if (!section.container) {
result = false;
extension.collector.error(nls.localize('navSection.missingContainer.error', "No container in nav section specified for extension."));
}
if (Object.keys(section.container).length !== 1) {
result = false;
extension.collector.error(nls.localize('navSection.moreThanOneDashboardContainersError', "Exactly 1 dashboard container must be defined per space."));
}
if (isValidIcon(section.icon, extension)) {
section.iconClass = createCSSRuleForIcon(section.icon, extension);
}
const containerKey = Object.keys(section.container)[0];
const containerValue = values(section.container)[0];
switch (containerKey) {
case WIDGETS_CONTAINER:
result = result && validateWidgetContainerContribution(extension, containerValue);
break;
case GRID_CONTAINER:
result = result && validateGridContainerContribution(extension, containerValue);
break;
case NAV_SECTION:
result = false;
extension.collector.error(nls.localize('navSection.invalidContainer.error', "NAV_SECTION within NAV_SECTION is an invalid container for extension."));
break;
}
});
return result;
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-nav-section {
height: 100%;
width: 100%;
display: block;
}

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* 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!./dashboardWebviewContainer';
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { WebviewContent } from 'sql/workbench/contrib/dashboard/browser/contents/webviewContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
@Component({
selector: 'dashboard-webview-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWebviewContainer) }],
template: `
<webview-content [webviewId]="tab.id">
</webview-content>
`
})
export class DashboardWebviewContainer extends DashboardTab implements AfterContentInit {
@Input() private tab: TabConfig;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(WebviewContent) private _webviewContent: WebviewContent;
constructor() {
super();
}
ngAfterContentInit(): void {
this._register(this._webviewContent.onResize(() => {
this._onResize.fire();
}));
}
public layout(): void {
this._webviewContent.layout();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public refresh(): void {
// no op
}
}

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 { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
export const WEBVIEW_CONTAINER = 'webview-container';
let webviewSchema: IJSONSchema = {
type: 'null',
description: nls.localize('dashboard.container.webview', "The webview that will be displayed in this tab."),
default: null
};
registerContainerType(WEBVIEW_CONTAINER, webviewSchema);
registerNavSectionContainerType(WEBVIEW_CONTAINER, webviewSchema);

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-webview-container {
height: 100%;
width : 100%;
display: block;
}

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 'vs/css!./dashboardWidgetContainer';
import { Component, Inject, Input, forwardRef, ViewChild, OnDestroy, ChangeDetectorRef, AfterContentInit } from '@angular/core';
import { TabConfig, WidgetConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
import { WidgetContent } from 'sql/workbench/contrib/dashboard/browser/contents/widgetContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Event, Emitter } from 'vs/base/common/event';
import { values } from 'vs/base/common/collections';
@Component({
selector: 'dashboard-widget-container',
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWidgetContainer) }],
template: `
<widget-content [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
</widget-content>
`
})
export class DashboardWidgetContainer extends DashboardTab implements OnDestroy, AfterContentInit {
@Input() protected tab: TabConfig;
protected widgets: WidgetConfig[];
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
@ViewChild(WidgetContent) protected _widgetContent: WidgetContent;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
ngOnInit() {
if (this.tab.container) {
this.widgets = values(this.tab.container)[0];
this._cd.detectChanges();
}
}
ngAfterContentInit(): void {
this._register(this._widgetContent.onResize(() => {
this._onResize.fire();
}));
}
ngOnDestroy() {
this.dispose();
}
public get id(): string {
return this.tab.id;
}
public get editable(): boolean {
return this.tab.editable;
}
public layout() {
this._widgetContent.layout();
}
public refresh(): void {
this._widgetContent.refresh();
}
public enableEdit(): void {
this._widgetContent.enableEdit();
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { generateDashboardWidgetSchema } from 'sql/workbench/contrib/dashboard/browser/pages/dashboardPageContribution';
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { find } from 'vs/base/common/arrays';
export const WIDGETS_CONTAINER = 'widgets-container';
const widgetsSchema: IJSONSchema = {
type: 'array',
description: nls.localize('dashboard.container.widgets', "The list of widgets that will be displayed in this tab."),
items: generateDashboardWidgetSchema(undefined, true)
};
registerContainerType(WIDGETS_CONTAINER, widgetsSchema);
registerNavSectionContainerType(WIDGETS_CONTAINER, widgetsSchema);
export function validateWidgetContainerContribution(extension: IExtensionPointUser<any>, WidgetConfigs: object[]): boolean {
let result = true;
WidgetConfigs.forEach(widgetConfig => {
const allKeys = Object.keys(widgetConfig);
const widgetKey = find(allKeys, key => key === 'widget');
if (!widgetKey) {
result = false;
extension.collector.error(nls.localize('widgetContainer.invalidInputs', "The list of widgets is expected inside widgets-container for extension."));
}
});
return result;
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
dashboard-widget-container {
height: 100%;
width: 100%;
}

View File

@@ -0,0 +1,8 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<agentview-component #agent *ngIf="(controlType) === 'agent'"></agentview-component>

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 'vs/css!./controlHostContent';
import { Component, forwardRef, Input, Inject, ChangeDetectorRef, ViewChild } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
import * as azdata from 'azdata';
import { memoize } from 'vs/base/common/decorators';
import { AgentViewComponent } from '../../../jobManagement/browser/agentView.component';
@Component({
templateUrl: decodeURI(require.toUrl('./controlHostContent.component.html')),
selector: 'controlhost-content'
})
export class ControlHostContent {
@Input() private webviewId: string;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
private _onMessage = new Emitter<string>();
public readonly onMessage: Event<string> = this._onMessage.event;
private _type: string;
/* Children components */
@ViewChild('agent') private _agentViewComponent: AgentViewComponent;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
) {
}
public layout(): void {
this._agentViewComponent.layout();
}
public get id(): string {
return this.webviewId;
}
@memoize
public get connection(): azdata.connection.Connection {
const currentConnection = this._dashboardService.connectionManagementService.connectionInfo.connectionProfile;
const connection: azdata.connection.Connection = {
providerName: currentConnection.providerName,
connectionId: currentConnection.id,
options: currentConnection.options
};
return connection;
}
@memoize
public get serverInfo(): azdata.ServerInfo {
return this._dashboardService.connectionManagementService.connectionInfo.serverInfo;
}
public setControlType(type: string): void {
this._type = type;
this._changeRef.detectChanges();
}
public get controlType(): string {
return this._type;
}
public refresh() {
this._agentViewComponent.refresh = true;
}
}

Some files were not shown because too many files have changed in this diff Show More