Refresh master with initial release/0.24 snapshot (#332)

* Initial port of release/0.24 source code

* Fix additional headers

* Fix a typo in launch.json
This commit is contained in:
Karl Burtram
2017-12-15 15:38:57 -08:00
committed by GitHub
parent 271b3a0b82
commit 6ad0df0e3e
7118 changed files with 107999 additions and 56466 deletions

View File

@@ -8,15 +8,14 @@
import 'vs/css!./media/accountDialog';
import 'vs/css!sql/parts/accountManagement/common/media/accountActions';
import * as DOM from 'vs/base/browser/dom';
import { SplitView } from 'vs/base/browser/ui/splitview/splitview';
import { SplitView } from 'sql/base/browser/ui/splitview/splitview';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IListService } from 'vs/platform/list/browser/listService';
import { Button } from 'vs/base/browser/ui/button/button';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import Event, { Emitter } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachListStyler, attachButtonStyler } from 'vs/platform/theme/common/styler';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { ActionRunner } from 'vs/base/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -25,8 +24,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import * as data from 'data';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { AccountViewModel } from 'sql/parts/accountManagement/accountDialog/accountViewModel';
import { AddAccountAction } from 'sql/parts/accountManagement/common/accountActions';
import { AccountListRenderer, AccountListDelegate } from 'sql/parts/accountManagement/common/accountListRenderer';
@@ -67,7 +67,7 @@ export class AccountDialog extends Modal {
@IContextKeyService contextKeyService: IContextKeyService
) {
super(
localize('linkedAccounts', 'Linked Accounts'),
localize('linkedAccounts', 'Linked accounts'),
TelemetryKeys.Accounts,
partService,
telemetryService,
@@ -81,7 +81,7 @@ export class AccountDialog extends Modal {
this._actionRunner = new ActionRunner();
// Setup the event emitters
this._onAddAccountErrorEmitter = new Emitter<string>();
this._onAddAccountErrorEmitter = new Emitter<string>();
this._onCloseEmitter = new Emitter<void>();
// Create the view model and wire up the events

View File

@@ -18,8 +18,6 @@ import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
export class AccountListStatusbarItem implements IStatusbarItem {
private _rootElement: HTMLElement;
private _iconElement: HTMLElement;
private _toDispose: IDisposable[];
private _manageLinkedAccountAction: IAction;
@@ -32,11 +30,11 @@ export class AccountListStatusbarItem implements IStatusbarItem {
public render(container: HTMLElement): IDisposable {
// Create root element for account list
this._rootElement = append(container, $('.linked-account-staus'));
this._rootElement.title = ManageLinkedAccountAction.LABEL;
this._rootElement.onclick = () => this._onClick();
this._iconElement = append(this._rootElement, $('a.linked-account-status-selection'));
const rootElement = append(container, $('.linked-account-staus'));
const accountElement = append(rootElement, $('a.linked-account-status-selection'));
accountElement.title = ManageLinkedAccountAction.LABEL;
accountElement.onclick = () => this._onClick();
append(accountElement, $('.linked-account-icon'));
return combinedDisposable(this._toDispose);
}

View File

@@ -3,9 +3,14 @@
* 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 {
background: url('accounts_statusbar_inverse.svg') center center no-repeat;
background-size: 12px;
height: 12px !important;
.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;
background-color: rgb(255, 255, 255);
width: 12px;
height: 22px;
}
.statusbar-item .linked-account-staus a.linked-account-status-selection {
padding: 0 5px 0 5px;
}

View File

@@ -34,6 +34,7 @@ export class AccountPicker extends Disposable {
private _refreshContainer: HTMLElement;
private _listContainer: HTMLElement;
private _dropdown: DropdownList;
private _refreshAccountAction: RefreshAccountAction;
// EVENTING ////////////////////////////////////////////////////////////
private _addAccountCompleteEmitter: Emitter<void>;
@@ -62,13 +63,6 @@ export class AccountPicker extends Disposable {
this._addAccountStartEmitter = new Emitter<void>();
this._onAccountSelectionChangeEvent = new Emitter<data.Account>();
// Create an account list
let delegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT);
let accountRenderer = new AccountPickerListRenderer();
this._listContainer = DOM.$('div.account-list-container');
this._accountList = new List<data.Account>(this._listContainer, delegate, [accountRenderer]);
this._register(attachListStyler(this._accountList, this._themeService));
// 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 => {
@@ -76,8 +70,6 @@ export class AccountPicker extends Disposable {
this.updateAccountList(arg.accountList);
}
});
this.createAccountPickerComponent();
}
// PUBLIC METHODS //////////////////////////////////////////////////////
@@ -88,7 +80,18 @@ export class AccountPicker extends Disposable {
DOM.append(container, this._rootElement);
}
private createAccountPickerComponent() {
// PUBLIC METHODS //////////////////////////////////////////////////////
/**
* Create account picker component
*/
public createAccountPickerComponent() {
// Create an account list
let delegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT);
let accountRenderer = new AccountPickerListRenderer();
this._listContainer = DOM.$('div.account-list-container');
this._accountList = new List<data.Account>(this._listContainer, delegate, [accountRenderer]);
this._register(attachListStyler(this._accountList, this._themeService));
this._rootElement = DOM.$('div.account-picker-container');
// Create a dropdown for account picker
@@ -116,7 +119,8 @@ export class AccountPicker extends Disposable {
this._refreshContainer = DOM.append(this._rootElement, DOM.$('div.refresh-container'));
DOM.append(this._refreshContainer, DOM.$('div.icon warning'));
let actionBar = new ActionBar(this._refreshContainer, { animated: false });
actionBar.push(new RefreshAccountAction(RefreshAccountAction.ID, RefreshAccountAction.LABEL), { icon: false, label: true });
this._refreshAccountAction = this._instantiationService.createInstance(RefreshAccountAction);
actionBar.push(this._refreshAccountAction, { icon: false, label: true });
if (this._accountList.length > 0) {
this._accountList.setSelection([0]);
@@ -146,10 +150,12 @@ export class AccountPicker extends Disposable {
private onAccountSelectionChange(account: data.Account) {
this.viewModel.selectedAccount = account;
if (account && account.isStale) {
this._refreshAccountAction.account = account;
new Builder(this._refreshContainer).show();
} else {
new Builder(this._refreshContainer).hide();
}
this._onAccountSelectionChangeEvent.fire(account);
}
@@ -170,9 +176,9 @@ export class AccountPicker extends Disposable {
const badgeContent = DOM.append(badge, DOM.$('div.badge-content'));
const label = DOM.append(row, DOM.$('div.label'));
icon.className = 'icon';
// Set the account icon
icon.style.background = `url('data:${account.displayInfo.contextualLogo.light}')`;
icon.classList.add('icon', account.displayInfo.accountType);
// TODO: Pick between the light and dark logo
label.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.contextualDisplayName + ')';

View File

@@ -54,6 +54,7 @@ export class AccountPickerService implements IAccountPickerService {
// 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());

View File

@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/autoOAuthDialog';
import { Builder, $ } from 'vs/base/browser/builder';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IPartService } from 'vs/workbench/services/part/common/partService';
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 { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
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(
@IPartService partService: IPartService,
@IThemeService private _themeService: IThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(
'',
TelemetryKeys.AutoOAuth,
partService,
telemetryService,
contextKeyService,
{
isFlyout: true,
hasBackButton: true,
hasSpinner: true
}
);
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
this.backButton.addListener('click', () => 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('cancel', 'Cancel'), () => this.cancel());
this.registerListeners();
this._userCodeInputBox.disable();
this._websiteInputBox.disable();
}
protected layout(height?: number): void {
// NO OP
}
protected renderBody(container: HTMLElement) {
$().div({ class: 'auto-oauth-description-section new-section' }, (descriptionContainer) => {
this._descriptionElement = descriptionContainer.getHTMLElement();
});
let addAccountSection;
$().div({ class: 'auto-oauth-info-section new-section' }, (addAccountContainer) => {
addAccountSection = addAccountContainer.getHTMLElement();
this._userCodeInputBox = this.createInputBoxHelper(addAccountContainer, localize('userCode', 'User code'));
this._websiteInputBox = this.createInputBoxHelper(addAccountContainer, localize('website', 'Website'));
});
new Builder(container).div({ class: 'auto-oauth-dialog' }, (builder) => {
builder.append(this._descriptionElement);
builder.append(addAccountSection);
});
}
private createInputBoxHelper(container: Builder, label: string): InputBox {
let inputBox: InputBox;
container.div({ class: 'dialog-input-section' }, (inputContainer) => {
inputContainer.div({ class: 'dialog-label' }, (labelContainer) => {
labelContainer.innerHtml(label);
});
inputContainer.div({ class: 'dialog-input' }, (inputCellContainer) => {
inputBox = new InputBox(inputCellContainer.getHTMLElement(), this._contextViewService);
});
});
return inputBox;
}
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.showSpinner();
this._onHandleAddAccount.fire();
}
}
public cancel() {
this._onCancel.fire();
}
public close() {
this._copyAndOpenButton.enabled = true;
this._onCloseEvent.fire();
this.hideSpinner();
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,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { AutoOAuthDialog } from 'sql/parts/accountManagement/autoOAuthDialog/autoOAuthDialog';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
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
) {
this._providerId = null;
}
/**
* Open auto OAuth dialog
*/
public openAutoOAuthDialog(providerId: string, title: string, message: string, userCode: string, uri: string): Thenable<void> {
if (this._providerId !== null) {
// If a oauth flyout is already open, return an error
let 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 = null;
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private handleOnCancel(): void {
this._accountManagementService.cancelAutoOAuthDeviceCode(this._providerId);
}
private handleOnClose(): void {
this._providerId = null;
}
private handleOnAddAccount(): void {
this._accountManagementService.copyUserCodeAndOpenBrowser(this._userCode, this._uri);
}
}

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

@@ -36,8 +36,6 @@ export class AddAccountAction extends Action {
constructor(
private _providerId: string,
@IMessageService private _messageService: IMessageService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IAccountManagementService private _accountManagementService: IAccountManagementService
) {
super(AddAccountAction.ID, AddAccountAction.LABEL);
@@ -99,21 +97,25 @@ export class RemoveAccountAction extends Action {
type: 'question'
};
if (!this._messageService.confirm(confirm)) {
return TPromise.as(false);
}
let confirmPromise = this._messageService.confirm(confirm);
return new TPromise((resolve, reject) => {
self._accountManagementService.removeAccount(self._account.key)
.then(
(result) => { resolve(result); },
(err) => {
// Must handle here as this is an independent action
self._errorMessageService.showDialog(Severity.Error,
localize('removeAccountFailed', 'Failed to remove account'), err);
resolve(false);
}
);
return confirmPromise.then(confirmation => {
if (!confirmation.confirmed) {
return TPromise.as(false);
} else {
return new TPromise((resolve, reject) => {
self._accountManagementService.removeAccount(self._account.key)
.then(
(result) => { resolve(result); },
(err) => {
// Must handle here as this is an independent action
self._errorMessageService.showDialog(Severity.Error,
localize('removeAccountFailed', 'Failed to remove account'), err);
resolve(false);
}
);
});
}
});
}
}
@@ -144,15 +146,31 @@ export class ApplyFilterAction extends Action {
export class RefreshAccountAction extends Action {
public static ID = 'account.refresh';
public static LABEL = localize('refreshAccount', 'Reenter your credentials');
public account: data.Account;
constructor(
id: string,
label: string
@IAccountManagementService private _accountManagementService: IAccountManagementService
) {
super(id, label, 'refresh-account-action icon refresh');
super(RefreshAccountAction.ID, RefreshAccountAction.LABEL, 'refresh-account-action icon refresh');
}
public run(): TPromise<boolean> {
// Todo: refresh the account
return TPromise.as(true);
let self = this;
return new TPromise((resolve, reject) => {
if (self.account) {
self._accountManagementService.refreshAccount(self.account)
.then(
() => {
resolve(true);
},
err => {
error(`Error while refreshing account: ${err}`);
reject(err);
}
);
} else {
let errorMessage = localize('NoAccountToRefresh', 'There is no account to refresh');
reject(errorMessage);
}
});
}
}

View File

@@ -13,7 +13,7 @@ import { ActionBar, IActionOptions } from 'vs/base/browser/ui/actionbar/actionba
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { RemoveAccountAction, ApplyFilterAction, RefreshAccountAction } from 'sql/parts/accountManagement/common/accountActions';
import { RemoveAccountAction, RefreshAccountAction } from 'sql/parts/accountManagement/common/accountActions';
import * as data from 'data';
@@ -66,9 +66,7 @@ export class AccountPickerListRenderer implements IRenderer<data.Account, Accoun
public renderElement(account: data.Account, index: number, templateData: AccountListTemplate): void {
// Set the account icon
templateData.icon.classList.add('account-logo');
templateData.icon.style.background = `url('data:${account.displayInfo.contextualLogo.light}')`;
// TODO: Pick between the light and dark logo
templateData.icon.classList.add('account-logo', account.displayInfo.accountType);
templateData.contextualDisplayName.innerText = account.displayInfo.contextualDisplayName;
templateData.displayName.innerText = account.displayInfo.displayName;
@@ -114,9 +112,12 @@ export class AccountListRenderer extends AccountPickerListRenderer {
let actionOptions: IActionOptions = { icon: true, label: false };
if (account.isStale) {
templateData.actions.push(new RefreshAccountAction(RefreshAccountAction.ID, RefreshAccountAction.LABEL), actionOptions);
let refreshAction = this._instantiationService.createInstance(RefreshAccountAction);
refreshAction.account = account;
templateData.actions.push(refreshAction, actionOptions);
} else {
templateData.actions.push(new ApplyFilterAction(ApplyFilterAction.ID, ApplyFilterAction.LABEL), actionOptions);
// Todo: Will show filter action when API/GUI for filtering is implemented (#3022, #3024)
// templateData.actions.push(new ApplyFilterAction(ApplyFilterAction.ID, ApplyFilterAction.LABEL), actionOptions);
}
let removeAction = this._instantiationService.createInstance(RemoveAccountAction, account);

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
import { join } from 'path';
import { createCSSRule } from 'vs/base/browser/dom';
import URI from 'vs/base/common/uri';
import { ManageLinkedAccountAction } from 'sql/parts/accountManagement/accountListStatusbar/accountListStatusbarItem';
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
ManageLinkedAccountAction,
ManageLinkedAccountAction.ID,
ManageLinkedAccountAction.LABEL
),
ManageLinkedAccountAction.LABEL
);
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[]>('account-type', [], accountsContribution).setHandler(extensions => {
function handleCommand(account: IAccountContrib, extension: IExtensionPointUser<any>) {
let { icon, id } = account;
let iconClass: string;
if (icon) {
iconClass = id;
if (typeof icon === 'string') {
const path = join(extension.description.extensionFolderPath, icon);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(path).toString()}")`);
} else {
const light = join(extension.description.extensionFolderPath, icon.light);
const dark = join(extension.description.extensionFolderPath, icon.dark);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(light).toString()}")`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(dark).toString()}")`);
}
}
}
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

@@ -6,7 +6,6 @@
'use strict';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IResourceProviderService, IHandleFirewallRuleResult } from 'sql/parts/accountManagement/common/interfaces';
@@ -26,7 +25,6 @@ export class ResourceProviderService implements IResourceProviderService {
constructor(
@ITelemetryService private _telemetryService: ITelemetryService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IEnvironmentService private _environmentService: IEnvironmentService
) {
}
@@ -66,39 +64,33 @@ export class ResourceProviderService implements IResourceProviderService {
* Handle a firewall rule
*/
public handleFirewallRule(errorCode: number, errorMessage: string, connectionTypeId: string): Promise<IHandleFirewallRuleResult> {
if (!this._environmentService.isBuilt) {
let self = this;
return new Promise<IHandleFirewallRuleResult>((resolve, reject) => {
let handleFirewallRuleResult: IHandleFirewallRuleResult;
let promises = [];
if (self._providers) {
for (let key in self._providers) {
let provider = self._providers[key];
promises.push(provider.handleFirewallRule(errorCode, errorMessage, connectionTypeId)
.then(response => {
if (response.result) {
handleFirewallRuleResult = { canHandleFirewallRule: response.result, ipAddress: response.ipAddress, resourceProviderId: key };
}
},
() => { /* Swallow failures at getting accounts, we'll just hide that provider */
}));
}
let self = this;
return new Promise<IHandleFirewallRuleResult>((resolve, reject) => {
let handleFirewallRuleResult: IHandleFirewallRuleResult;
let promises = [];
if (self._providers) {
for (let key in self._providers) {
let provider = self._providers[key];
promises.push(provider.handleFirewallRule(errorCode, errorMessage, connectionTypeId)
.then(response => {
if (response.result) {
handleFirewallRuleResult = { canHandleFirewallRule: response.result, ipAddress: response.ipAddress, resourceProviderId: key };
}
},
() => { /* Swallow failures at getting accounts, we'll just hide that provider */
}));
}
}
Promise.all(promises).then(() => {
if (handleFirewallRuleResult) {
resolve(handleFirewallRuleResult);
} else {
handleFirewallRuleResult = { canHandleFirewallRule: false, ipAddress: undefined, resourceProviderId: undefined };
resolve(handleFirewallRuleResult);
}
});
Promise.all(promises).then(() => {
if (handleFirewallRuleResult) {
resolve(handleFirewallRuleResult);
} else {
handleFirewallRuleResult = { canHandleFirewallRule: false, ipAddress: undefined, resourceProviderId: undefined };
resolve(handleFirewallRuleResult);
}
});
} else {
return new Promise<IHandleFirewallRuleResult>((resolve, reject) => {
resolve({ canHandleFirewallRule: false, ipAddress: undefined, resourceProviderId: undefined });
});
}
});
}
/**

View File

@@ -8,24 +8,23 @@
import 'vs/css!./media/firewallRuleDialog';
import { Builder, $ } from 'vs/base/browser/builder';
import * as DOM from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { IPartService } from 'vs/workbench/services/part/common/partService';
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 { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { attachButtonStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import * as data from 'data';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { FirewallRuleViewModel } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleViewModel';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { IAccountPickerService } from 'sql/parts/accountManagement/common/interfaces';
import * as TelemetryKeys from 'sql/common/telemetryKeys';

View File

@@ -10,7 +10,7 @@ import { localize } from 'vs/nls';
import * as data from 'data';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IConnectionManagementService, IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { FirewallRuleDialog } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
import { IResourceProviderService } from 'sql/parts/accountManagement/common/interfaces';
@@ -29,7 +29,6 @@ export class FirewallRuleDialogController {
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@IAccountManagementService private _accountManagementService: IAccountManagementService,
@IErrorMessageService private _errorMessageService: IErrorMessageService
) {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IExtensionGalleryService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
@@ -12,16 +12,16 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import { DashboardEditor } from 'sql/parts/dashboard/dashboardEditor';
import { DashboardInput } from 'sql/parts/dashboard/dashboardInput';
import { AddServerGroupAction, AddServerAction } from 'sql/parts/registeredServer/viewlet/connectionTreeAction';
import { ClearRecentConnectionsAction } from 'sql/parts/connection/common/connectionActions';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService';
import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorDescriptor } from 'vs/workbench/browser/editor';
import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService';
import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actionRegistry';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { localize } from 'vs/nls';
import { AddServerGroupAction, AddServerAction } from 'sql/parts/registeredServer/viewlet/connectionTreeAction';
// Singletons
registerSingleton(IExtensionGalleryService, ExtensionGalleryService);
@@ -30,10 +30,9 @@ registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
// Connection Dashboard registration
const dashboardEditorDescriptor = new EditorDescriptor(
DashboardEditor,
DashboardEditor.ID,
'Dashboard',
'sql/parts/dashboard/dashboardEditor',
'DashboardEditor'
'Dashboard'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
@@ -50,7 +49,6 @@ actionRegistry.registerWorkbenchAction(
),
ClearRecentConnectionsAction.LABEL
);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
AddServerGroupAction,
@@ -82,8 +80,8 @@ configurationRegistry.registerConfiguration({
},
'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.'),
'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'
},
}
});
});

View File

@@ -3,57 +3,84 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import Event, { Emitter } from 'vs/base/common/event';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
/**
* Locates the active editor and calls runQuery() on the editor if it is a QueryEditor.
* Workbench action to clear the recent connnections list
*/
export class ClearRecentConnectionsAction extends Action {
public static ID = 'clearRecentConnectionsAction';
public static LABEL = nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List');
public static ID = 'clearRecentConnectionsAction';
public static LABEL = nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List');
constructor(
id: string,
label: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IMessageService private _messageService: IMessageService,
@IQuickOpenService private _quickOpenService: IQuickOpenService
) {
super(id, label);
this.enabled = true;
}
public run(): TPromise<void> {
let self = this;
return self.promptToClearRecentConnectionsList().then(result => {
if (result) {
self._connectionManagementService.clearRecentConnectionsList();
self._messageService.show(Severity.Info, nls.localize('ClearedRecentConnections', 'Recent connections list cleared'));
}
});
}
private promptToClearRecentConnectionsList(): TPromise<boolean> {
const self = this;
return new TPromise<boolean>((resolve, reject) => {
let choices: { key, value }[] = [
{ key: nls.localize('yes', 'Yes'), value: true },
{ key: nls.localize('no', 'No'), value: false }
];
self._quickOpenService.pick(choices.map(x => x.key), { placeHolder: nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List'), ignoreFocusLost: true }).then((choice) => {
let confirm = choices.find(x => x.key === choice);
resolve(confirm && confirm.value);
});
});
}
}
/**
* 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,
@IInstantiationService private _instantiationService: IInstantiationService,
private _connectionProfile: IConnectionProfile,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IMessageService private _messageService: IMessageService,
@IQuickOpenService private _quickOpenService: IQuickOpenService
) {
super(id, label);
this.enabled = true;
}
public run(): TPromise<void> {
let self = this;
return self.promptToClearRecentConnectionsList().then(result => {
if (result) {
self._connectionManagementService.clearRecentConnectionsList();
self._messageService.show(Severity.Info, nls.localize('ClearedRecentConnections', 'Recent connections list cleared'));
}
return new TPromise<void>((resolve, reject) => {
resolve(this._connectionManagementService.clearRecentConnection(this._connectionProfile));
this._onRecentConnectionRemoved.fire();
});
}
private promptToClearRecentConnectionsList(): TPromise<boolean> {
const self = this;
return new TPromise<boolean>((resolve, reject) => {
let choices: { key, value }[] = [
{ key: nls.localize('yes', 'Yes'), value: true },
{ key: nls.localize('no', 'No'), value: false }
];
self._quickOpenService.pick(choices.map(x => x.key), { placeHolder: nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List'), ignoreFocusLost: true }).then((choice) => {
let confirm = choices.find(x => x.key === choice);
resolve(confirm && confirm.value);
});
});
}
}
}

View File

@@ -9,9 +9,9 @@ import * as Utils from './utils';
import { IConnectionProfile, IConnectionProfileStore } from './interfaces';
import { IConnectionConfig } from './iconnectionConfig';
import { ConnectionProfileGroup, IConnectionProfileGroup } from './connectionProfileGroup';
import { IConfigurationEditingService, ConfigurationTarget, IConfigurationValue } from 'vs/workbench/services/configuration/common/configurationEditing';
import { ConfigurationEditingService, IConfigurationValue, IConfigurationValue as TConfigurationValue } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IConfigurationValue as TConfigurationValue } from 'vs/platform/configuration/common/configuration';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ConnectionProfile } from './connectionProfile';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import * as data from 'data';
@@ -30,22 +30,25 @@ export interface ISaveGroupResult {
export class ConnectionConfig implements IConnectionConfig {
private _providerCapabilitiesMap: { [providerName: string]: data.DataProtocolServerCapabilities };
private _providerCachedCapabilitiesMap: { [providerName: string]: data.DataProtocolServerCapabilities };
/**
* Constructor.
*/
public constructor(
private _configurationEditService: IConfigurationEditingService,
private _configurationEditService: ConfigurationEditingService,
private _workspaceConfigurationService: IWorkspaceConfigurationService,
private _capabilitiesService: ICapabilitiesService,
private _cachedMetadata?: data.DataProtocolServerCapabilities[]
cachedMetadata?: data.DataProtocolServerCapabilities[]
) {
this._providerCapabilitiesMap = {};
this._providerCachedCapabilitiesMap = {};
this.setCachedMetadata(cachedMetadata);
}
public setCachedMetadata(cachedMetaData: data.DataProtocolServerCapabilities[]): void {
this._cachedMetadata = cachedMetaData;
public setCachedMetadata(cachedMetadata: data.DataProtocolServerCapabilities[]): void {
if (cachedMetadata) {
cachedMetadata.forEach(item => {
this.updateCapabilitiesCache(item.providerName, item);
});
}
}
/**
@@ -58,7 +61,6 @@ export class ConnectionConfig implements IConnectionConfig {
let workspaceGroups = this.getConfiguration(Constants.connectionGroupsArrayName).workspace as IConnectionProfileGroup[];
if (userGroups) {
if (workspaceGroups) {
userGroups = userGroups.filter(x => workspaceGroups.find(f => this.isSameGroupName(f, x)) === undefined);
allGroups = allGroups.concat(workspaceGroups);
@@ -74,43 +76,44 @@ export class ConnectionConfig implements IConnectionConfig {
return allGroups;
}
private updateCapabilitiesCache(providerName: string, providerCapabilities: data.DataProtocolServerCapabilities): void {
if (providerName && providerCapabilities) {
this._providerCapabilitiesMap[providerName] = providerCapabilities;
}
}
private getCapabilitiesFromCache(providerName: string): data.DataProtocolServerCapabilities {
if (providerName in this._providerCapabilitiesMap) {
return this._providerCapabilitiesMap[providerName];
}
return undefined;
}
/**
* Returns the capabilities for given provider name. First tries to get it from capabilitiesService and if it's not registered yet,
* Gets the data from the metadata stored in the config
* @param providerName Provider Name
*/
public getCapabilities(providerName: string): data.DataProtocolServerCapabilities {
let result: data.DataProtocolServerCapabilities;
if (providerName in this._providerCapabilitiesMap) {
result = this._providerCapabilitiesMap[providerName];
let result: data.DataProtocolServerCapabilities = this.getCapabilitiesFromCache(providerName);
if (result) {
return result;
} else {
let capabilities = this._capabilitiesService.getCapabilities();
if (capabilities) {
let providerCapabilities = capabilities.find(c => c.providerName === providerName);
if (providerCapabilities) {
this._providerCapabilitiesMap[providerName] = providerCapabilities;
result = providerCapabilities;
this.updateCapabilitiesCache(providerName, providerCapabilities);
return providerCapabilities;
} else {
return undefined;
}
}
}
if (!result && this._cachedMetadata) {
if (providerName in this._providerCachedCapabilitiesMap) {
result = this._providerCachedCapabilitiesMap[providerName];
} else {
let metaDataFromConfig = this._cachedMetadata;
if (metaDataFromConfig) {
let providerCapabilities = metaDataFromConfig.find(m => m.providerName === providerName);
this._providerCachedCapabilitiesMap[providerName] = providerCapabilities;
result = providerCapabilities;
}
return undefined;
}
}
return result;
}
/**
* Add a new connection to the connection config.
*/
@@ -118,13 +121,13 @@ export class ConnectionConfig implements IConnectionConfig {
return new Promise<IConnectionProfile>((resolve, reject) => {
if (profile.saveProfile) {
this.addGroupFromProfile(profile).then(groupId => {
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
let profiles = this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
if (!profiles) {
profiles = [];
}
let providerCapabilities = this.getCapabilities(profile.providerName);
let connectionProfile = this.getConnectionProfileInstance(profile, groupId);
let connectionProfile = this.getConnectionProfileInstance(profile, groupId, providerCapabilities);
let newProfile = ConnectionProfile.convertToProfileStore(providerCapabilities, connectionProfile);
// Remove the profile if already set
@@ -151,9 +154,8 @@ export class ConnectionConfig implements IConnectionConfig {
});
}
private getConnectionProfileInstance(profile: IConnectionProfile, groupId: string): ConnectionProfile {
private getConnectionProfileInstance(profile: IConnectionProfile, groupId: string, providerCapabilities: data.DataProtocolServerCapabilities): ConnectionProfile {
let connectionProfile = profile as ConnectionProfile;
let providerCapabilities = this.getCapabilities(profile.providerName);
if (connectionProfile === undefined) {
connectionProfile = new ConnectionProfile(providerCapabilities, profile);
}
@@ -170,7 +172,7 @@ export class ConnectionConfig implements IConnectionConfig {
if (profile.groupId && profile.groupId !== Utils.defaultGroupId) {
resolve(profile.groupId);
} else {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let result = this.saveGroup(groups, profile.groupFullName, undefined, undefined);
groups = result.groups;
@@ -192,7 +194,7 @@ export class ConnectionConfig implements IConnectionConfig {
if (profileGroup.id) {
resolve(profileGroup.id);
} else {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let sameNameGroup = groups ? groups.find(group => group.name === profileGroup.name) : undefined;
if (sameNameGroup) {
let errMessage: string = nls.localize('invalidServerName', "A server group with the same name already exists.");
@@ -221,7 +223,7 @@ export class ConnectionConfig implements IConnectionConfig {
profiles = <IConnectionProfileStore[]>configs.workspace;
}
if (profiles) {
if(this.fixConnectionIds(profiles)) {
if (this.fixConnectionIds(profiles)) {
this.writeConfiguration(Constants.connectionsArrayName, profiles, configTarget);
}
} else {
@@ -245,9 +247,9 @@ export class ConnectionConfig implements IConnectionConfig {
profile.id = generateUuid();
changed = true;
}
if (profile.id in idsCache) {
profile.id = generateUuid();
changed = true;
if (profile.id in idsCache) {
profile.id = generateUuid();
changed = true;
}
idsCache[profile.id] = true;
}
@@ -298,7 +300,7 @@ export class ConnectionConfig implements IConnectionConfig {
*/
public deleteConnection(profile: ConnectionProfile): Promise<void> {
// Get all connections in the settings
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
let profiles = this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
// Remove the profile from the connections
profiles = profiles.filter(value => {
let providerCapabilities = this.getCapabilities(value.providerName);
@@ -320,7 +322,7 @@ export class ConnectionConfig implements IConnectionConfig {
// Add selected group to subgroups list
subgroups.push(group);
// Get all connections in the settings
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
let profiles = this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
// Remove the profiles from the connections
profiles = profiles.filter(value => {
let providerCapabilities = this.getCapabilities(value.providerName);
@@ -329,7 +331,7 @@ export class ConnectionConfig implements IConnectionConfig {
});
// Get all groups in the settings
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
// Remove subgroups in the settings
groups = groups.filter((grp) => {
return !subgroups.some((item) => item.id === grp.id);
@@ -347,7 +349,7 @@ export class ConnectionConfig implements IConnectionConfig {
* Moves the source group under the target group.
*/
public changeGroupIdForConnectionGroup(source: ConnectionProfileGroup, target: ConnectionProfileGroup): Promise<void> {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
groups = groups.map(g => {
if (g.id === source.id) {
g.parentId = target.id;
@@ -372,8 +374,8 @@ export class ConnectionConfig implements IConnectionConfig {
*/
private changeGroupIdForConnectionInSettings(profile: ConnectionProfile, newGroupID: string, target: ConfigurationTarget = ConfigurationTarget.USER): Promise<void> {
return new Promise<void>((resolve, reject) => {
let profiles = target === ConfigurationTarget.USER ? this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user :
this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).workspace;
let profiles = target === ConfigurationTarget.USER ? this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user :
this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).workspace;
if (profiles) {
let providerCapabilities = this.getCapabilities(profile.providerName);
if (profile.parent && profile.parent.id === Constants.unsavedGroupId) {
@@ -429,7 +431,7 @@ export class ConnectionConfig implements IConnectionConfig {
}
public editGroup(source: ConnectionProfileGroup): Promise<void> {
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let sameNameGroup = groups ? groups.find(group => group.name === source.name && group.id !== source.id) : undefined;
if (sameNameGroup) {
let errMessage: string = nls.localize('invalidServerName', "A server group with the same name already exists.");
@@ -510,10 +512,9 @@ export class ConnectionConfig implements IConnectionConfig {
* @param parsedSettingsFile an object representing the parsed contents of the settings file.
* @returns the set of connection profiles found in the parsed settings file.
*/
private getConfiguration(key: string): TConfigurationValue<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]> {
let configs: TConfigurationValue<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]>;
configs = this._workspaceConfigurationService.lookup<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]>(key);
private getConfiguration(key: string): any {
let configs: any;
configs = this._workspaceConfigurationService.inspect<IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[]>(key);
return configs;
}
@@ -532,7 +533,7 @@ export class ConnectionConfig implements IConnectionConfig {
value: profiles
};
this._configurationEditService.writeConfiguration(target, configValue).then(result => {
this._workspaceConfigurationService.reloadConfiguration().then(() => {
this._workspaceConfigurationService.reloadConfiguration().then(() => {
resolve();
});
}, (error => {

View File

@@ -9,12 +9,12 @@ import { IViewlet } from 'vs/workbench/common/viewlet';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import Event from 'vs/base/common/event';
import { IAction } from 'vs/base/common/actions';
import Severity from 'vs/base/common/severity';
import data = require('data');
import { IConnectionProfileGroup, ConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import Severity from 'vs/base/common/severity';
import { ISelectionData } from 'data';
import { ConnectionManagementInfo } from './connectionManagementInfo';
export const VIEWLET_ID = 'workbench.view.connections';
@@ -57,6 +57,7 @@ export interface IConnectionResult {
connected: boolean;
errorMessage: string;
errorCode: number;
callStack: string;
errorHandled?: boolean;
}
@@ -85,7 +86,7 @@ export interface IConnectionManagementService {
/**
* Opens the connection dialog to create new connection
*/
showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, error?: string): Promise<void>;
showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Promise<void>;
/**
* Opens the add server group dialog
@@ -135,6 +136,8 @@ export interface IConnectionManagementService {
clearRecentConnectionsList(): void;
clearRecentConnection(connectionProfile: IConnectionProfile) : void;
getActiveConnections(): ConnectionProfile[];
saveProfileGroup(profile: IConnectionProfileGroup): Promise<string>;
@@ -253,7 +256,7 @@ export interface IConnectionManagementService {
export const IConnectionDialogService = createDecorator<IConnectionDialogService>('connectionDialogService');
export interface IConnectionDialogService {
_serviceBrand: any;
showDialog(connectionManagementService: IConnectionManagementService, params: INewConnectionParams, model: IConnectionProfile, error?: string): Thenable<void>;
showDialog(connectionManagementService: IConnectionManagementService, params: INewConnectionParams, model: IConnectionProfile, connectionResult?: IConnectionResult): Thenable<void>;
}
export interface IServerGroupDialogCallbacks {
@@ -270,7 +273,7 @@ export interface IServerGroupController {
export const IErrorMessageService = createDecorator<IErrorMessageService>('errorMessageService');
export interface IErrorMessageService {
_serviceBrand: any;
showDialog(severity: Severity, headerTitle: string, message: string): void;
showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[]): void;
}
export enum ServiceOptionType {
@@ -295,14 +298,15 @@ export enum RunQueryOnConnectionMode {
none = 0,
executeQuery = 1,
executeCurrentQuery = 2,
estimatedQueryPlan = 3
estimatedQueryPlan = 3,
actualQueryPlan = 4
}
export interface INewConnectionParams {
connectionType: ConnectionType;
input?: IConnectableInput;
runQueryOnCompletion?: RunQueryOnConnectionMode;
querySelection?: ISelectionData;
querySelection?: data.ISelectionData;
showDashboard?: boolean;
}

View File

@@ -29,7 +29,7 @@ export class ConnectionManagementInfo {
/**
* Callback for when a connection notification is received.
*/
public connectHandler: (result: boolean, errorMessage?: string, errorCode?: number) => void;
public connectHandler: (result: boolean, errorMessage?: string, errorCode?: number, callStack?: string) => void;
/**
* Information about the SQL Server instance.

View File

@@ -43,7 +43,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ConnectionProfileGroup, IConnectionProfileGroup } from './connectionProfileGroup';
import { IConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditing';
import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import Event, { Emitter } from 'vs/base/common/event';
@@ -78,6 +78,8 @@ export class ConnectionManagementService implements IConnectionManagementService
private _connectionGlobalStatus: ConnectionGlobalStatus;
private _configurationEditService: ConfigurationEditingService;
constructor(
private _connectionMemento: Memento,
private _connectionStore: ConnectionStore,
@@ -89,7 +91,6 @@ export class ConnectionManagementService implements IConnectionManagementService
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
@IStorageService private _storageService: IStorageService,
@ITelemetryService private _telemetryService: ITelemetryService,
@IConfigurationEditingService private _configurationEditService: IConfigurationEditingService,
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
@ICredentialsService private _credentialsService: ICredentialsService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@@ -100,13 +101,17 @@ export class ConnectionManagementService implements IConnectionManagementService
@IViewletService private _viewletService: IViewletService,
@IAngularEventingService private _angularEventing: IAngularEventingService
) {
if (this._instantiationService) {
this._configurationEditService = this._instantiationService.createInstance(ConfigurationEditingService);
}
// _connectionMemento and _connectionStore are in constructor to enable this class to be more testable
if (!this._connectionMemento) {
this._connectionMemento = new Memento('ConnectionManagement');
}
if (!this._connectionStore) {
this._connectionStore = new ConnectionStore(_storageService, this._connectionMemento,
_configurationEditService, this._workspaceConfigurationService, this._credentialsService, this._capabilitiesService);
this._configurationEditService, this._workspaceConfigurationService, this._credentialsService, this._capabilitiesService);
}
this._connectionStatusManager = new ConnectionStatusManager(this._capabilitiesService);
@@ -196,7 +201,7 @@ export class ConnectionManagementService implements IConnectionManagementService
// show the Registered Server viewlet
let startupConfig = this._workspaceConfigurationService.getConfiguration('startup');
if (startupConfig) {
let showServerViewlet = <boolean>startupConfig['alwaysShowServersView'];
let showServerViewlet = <boolean>startupConfig['alwaysShowServersView'];
if (showServerViewlet) {
// only show the Servers viewlet if there isn't another active viewlet
if (!this._viewletService.getActiveViewlet()) {
@@ -212,7 +217,7 @@ export class ConnectionManagementService implements IConnectionManagementService
* @param params Include the uri, type of connection
* @param model the existing connection profile to create a new one from
*/
public showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, error?: string): Promise<void> {
public showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Promise<void> {
let self = this;
return new Promise<void>((resolve, reject) => {
if (!params) {
@@ -221,7 +226,7 @@ export class ConnectionManagementService implements IConnectionManagementService
if (!model && params.input && params.input.uri) {
model = this._connectionStatusManager.getConnectionProfile(params.input.uri);
}
self._connectionDialogService.showDialog(self, params, model, error).then(() => {
self._connectionDialogService.showDialog(self, params, model, connectionResult).then(() => {
resolve();
}, dialogError => {
warn('failed to open the connection dialog. error: ' + dialogError);
@@ -301,7 +306,7 @@ export class ConnectionManagementService implements IConnectionManagementService
}
// If the password is required and still not loaded show the dialog
if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
resolve(this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, errorCode: undefined }, options));
resolve(this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
} else {
// Try to connect
this.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => {
@@ -340,7 +345,7 @@ export class ConnectionManagementService implements IConnectionManagementService
runQueryOnCompletion: RunQueryOnConnectionMode.none,
showDashboard: options.showDashboard
};
this.showConnectionDialog(params, connection, connectionResult.errorMessage).then(() => {
this.showConnectionDialog(params, connection, connectionResult).then(() => {
resolve(connectionResult);
}).catch(err => {
reject(err);
@@ -643,6 +648,10 @@ export class ConnectionManagementService implements IConnectionManagementService
return this._connectionStore.clearRecentlyUsed();
}
public clearRecentConnection(connectionProfile: IConnectionProfile) : void {
this._connectionStore.removeConnectionToMemento(connectionProfile, Constants.recentConnections);
}
public getActiveConnections(): ConnectionProfile[] {
return this._connectionStore.getActiveConnections();
}
@@ -825,6 +834,9 @@ export class ConnectionManagementService implements IConnectionManagementService
return new Promise<data.ListDatabasesResult>((resolve, reject) => {
let provider = this._providers[providerId];
provider.listDatabases(uri).then(result => {
if (result && result.databaseNames) {
result.databaseNames.sort();
}
resolve(result);
}, error => {
reject(error);
@@ -845,9 +857,9 @@ export class ConnectionManagementService implements IConnectionManagementService
/**
* Add a connection to the active connections list.
*/
private tryAddActiveConnection(connectionManagementInfo: ConnectionManagementInfo, newConnection: IConnectionProfile): void {
private tryAddActiveConnection(connectionManagementInfo: ConnectionManagementInfo, newConnection: IConnectionProfile, isConnectionToDefaultDb: boolean): void {
if (newConnection) {
this._connectionStore.addActiveConnection(newConnection)
this._connectionStore.addActiveConnection(newConnection, isConnectionToDefaultDb)
.then(() => {
connectionManagementInfo.connectHandler(true);
}, err => {
@@ -881,6 +893,10 @@ export class ConnectionManagementService implements IConnectionManagementService
let connection = this._connectionStatusManager.onConnectionComplete(info);
if (info.connectionId) {
let isConnectionToDefaultDb = false;
if (connection.connectionProfile && (!connection.connectionProfile.databaseName || connection.connectionProfile.databaseName.trim() === '')) {
isConnectionToDefaultDb = true;
}
if (info.connectionSummary && info.connectionSummary.databaseName) {
this._connectionStatusManager.updateDatabaseName(info);
}
@@ -889,14 +905,14 @@ export class ConnectionManagementService implements IConnectionManagementService
connection.connectHandler(true);
let activeConnection = connection.connectionProfile;
self.tryAddActiveConnection(connection, activeConnection);
self.tryAddActiveConnection(connection, activeConnection, isConnectionToDefaultDb);
self.addTelemetryForConnection(connection);
} else {
connection.connectHandler(false, info.messages, info.errorNumber);
}
if (this._connectionStatusManager.isDefaultTypeUri(info.ownerUri)) {
this._connectionGlobalStatus.setStatusToConnected(info.connectionSummary);
if (self._connectionStatusManager.isDefaultTypeUri(info.ownerUri)) {
self._connectionGlobalStatus.setStatusToConnected(info.connectionSummary);
}
} else {
connection.connectHandler(false, info.errorMessage, info.errorNumber, info.messages);
}
}
@@ -1004,18 +1020,18 @@ export class ConnectionManagementService implements IConnectionManagementService
this._capabilitiesService.onCapabilitiesReady().then(() => {
let connectionInfo = this._connectionStatusManager.addConnection(connection, uri);
// Setup the handler for the connection complete notification to call
connectionInfo.connectHandler = ((connectResult, errorMessage, errorCode) => {
connectionInfo.connectHandler = ((connectResult, errorMessage, errorCode, callStack) => {
let connectionMngInfo = this._connectionStatusManager.findConnection(uri);
if (connectionMngInfo && connectionMngInfo.deleted) {
this._connectionStatusManager.deleteConnection(uri);
resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, errorHandled: true });
resolve({ connected: connectResult, errorMessage: undefined, errorCode: undefined, callStack: undefined, errorHandled: true });
} else {
if (errorMessage) {
// Connection to the server failed
this._connectionStatusManager.deleteConnection(uri);
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode });
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack });
} else {
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode });
resolve({ connected: connectResult, errorMessage: errorMessage, errorCode: errorCode, callStack: callStack });
}
}
});

View File

@@ -79,6 +79,26 @@ export class ConnectionProfileGroup implements IConnectionProfileGroup {
return false;
}
public get hasValidConnections(): boolean {
if (this.connections) {
let invalidConnections = this.connections.find(c => c.serverCapabilities === undefined);
if (invalidConnections !== undefined) {
return false;
} else {
let childrenAreValid: boolean = true;
this.children.forEach(element => {
let isChildValid = element.hasValidConnections;
if (!isChildValid) {
childrenAreValid = false;
}
});
return childrenAreValid;
}
} else {
return true;
}
}
public getChildren(): any {
let allChildren = [];

View File

@@ -155,9 +155,9 @@ export class ConnectionStatusManager {
}
/**
* Tries to find an existing connection that's mapped with given the ownerUri
* Tries to find an existing connection that's mapped with the given ownerUri
* The purpose for this method is to find the connection given the ownerUri and find the original uri assigned to it. most of the times should be the same.
* Only if the db name in the original uri is different than when connection is complete, we need to use the original uri
* Only if the db name in the original uri is different when connection is complete, we need to use the original uri
* Returns the generated ownerUri for the connection profile if not existing connection found
* @param ownerUri connection owner uri to find an existing connection
* @param purpose purpose for the connection

View File

@@ -15,11 +15,11 @@ import { ConnectionConfig } from './connectionConfig';
import { Memento, Scope as MementoScope } from 'vs/workbench/common/memento';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ConnectionProfileGroup, IConnectionProfileGroup } from './connectionProfileGroup';
import { IConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditing';
import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import * as data from 'data';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
const MAX_CONNECTIONS_DEFAULT = 25;
@@ -37,24 +37,21 @@ export class ConnectionStore {
constructor(
private _storageService: IStorageService,
private _context: Memento,
private _configurationEditService: IConfigurationEditingService,
private _configurationEditService: ConfigurationEditingService,
private _workspaceConfigurationService: IWorkspaceConfigurationService,
private _credentialService: ICredentialsService,
private _capabilitiesService: ICapabilitiesService,
private _connectionConfig?: IConnectionConfig
) {
if (_context) {
this._memento = this._context.getMemento(this._storageService, MementoScope.GLOBAL);
}
this._groupIdToFullNameMap = {};
this._groupFullNameToIdMap = {};
if (!this._connectionConfig) {
let cachedServerCapabilities = this.getCachedServerCapabilities();
this._connectionConfig = new ConnectionConfig(this._configurationEditService,
this._workspaceConfigurationService, this._capabilitiesService, cachedServerCapabilities);
this._connectionConfig.setCachedMetadata(cachedServerCapabilities);
}
}
@@ -115,17 +112,6 @@ export class ConnectionStore {
}
}
/**
* Gets all connection profiles stored in the user settings
* Profiles from workspace will be included if getWorkspaceProfiles is passed as true
* Note: connections will not include password value
*
* @returns {IConnectionProfile[]}
*/
public getProfiles(getWorkspaceProfiles: boolean): IConnectionProfile[] {
return this.loadProfiles(getWorkspaceProfiles);
}
public addSavedPassword(credentialsItem: IConnectionProfile): Promise<{ profile: IConnectionProfile, savedCred: boolean }> {
let self = this;
return new Promise<{ profile: IConnectionProfile, savedCred: boolean }>((resolve, reject) => {
@@ -314,12 +300,15 @@ export class ConnectionStore {
* @param {IConnectionCredentials} conn the connection to add
* @returns {Promise<void>} a Promise that returns when the connection was saved
*/
public addActiveConnection(conn: IConnectionProfile): Promise<void> {
if(this.getActiveConnections().some(existingConn => existingConn.id === conn.id)) {
public addActiveConnection(conn: IConnectionProfile, isConnectionToDefaultDb: boolean = false): Promise<void> {
if (this.getActiveConnections().some(existingConn => existingConn.id === conn.id)) {
return Promise.resolve(undefined);
} else {
return this.addConnectionToMemento(conn, Constants.activeConnections, undefined, conn.savePassword).then(() => {
let maxConnections = this.getMaxRecentConnectionsCount();
if (isConnectionToDefaultDb) {
conn.databaseName = '';
}
return this.addConnectionToMemento(conn, Constants.recentConnections, maxConnections);
});
}
@@ -330,8 +319,7 @@ export class ConnectionStore {
return new Promise<void>((resolve, reject) => {
// Get all profiles
let configValues = self.getConnectionsFromMemento(mementoKey);
let configToSave: IConnectionProfile[] = this.addToConnectionList(conn, configValues, mementoKey === Constants.recentConnections);
let configToSave = this.addToConnectionList(conn, configValues);
if (maxConnections) {
// Remove last element if needed
if (configToSave.length > maxConnections) {
@@ -349,24 +337,6 @@ export class ConnectionStore {
});
}
private isSameConnectionProfileNoGroup(profile1: IConnectionProfile, profile2: IConnectionProfile): boolean {
// both are undefined
if (!profile1 && !profile2) {
return true;
}
// only one is undefined
if (!profile1 || !profile2) {
return false;
}
// compare all the connection's "identity" properties
return equalsIgnoreCase(profile1.serverName, profile2.serverName) &&
equalsIgnoreCase(profile1.databaseName, profile2.databaseName) &&
equalsIgnoreCase(profile1.userName, profile2.userName) &&
profile1.authenticationType === profile2.authenticationType &&
profile1.providerName === profile2.providerName;
}
public removeConnectionToMemento(conn: IConnectionProfile, mementoKey: string): Promise<void> {
const self = this;
return new Promise<void>((resolve, reject) => {
@@ -388,25 +358,18 @@ export class ConnectionStore {
return this.convertConfigValuesToConnectionProfiles(configValues);
}
private addToConnectionList(conn: IConnectionProfile, list: ConnectionProfile[], isRecentConnections: boolean): IConnectionProfile[] {
private addToConnectionList(conn: IConnectionProfile, list: ConnectionProfile[]): IConnectionProfile[] {
let savedProfile: ConnectionProfile = this.getProfileWithoutPassword(conn);
// Remove the connection from the list if it already exists
if (isRecentConnections) {
// recent connections should use a different comparison the server viewlet for managing connection list
list = list.filter(value => {
return !(this.isSameConnectionProfileNoGroup(value, savedProfile));
});
} else {
list = list.filter(value => {
let equal = value && value.getConnectionInfoId() === savedProfile.getConnectionInfoId();
if (equal && savedProfile.saveProfile) {
equal = value.groupId === savedProfile.groupId ||
ConnectionProfileGroup.sameGroupName(value.groupFullName, savedProfile.groupFullName);
}
return !equal;
});
}
list = list.filter(value => {
let equal = value && value.getConnectionInfoId() === savedProfile.getConnectionInfoId();
if (equal && savedProfile.saveProfile) {
equal = value.groupId === savedProfile.groupId ||
ConnectionProfileGroup.sameGroupName(value.groupFullName, savedProfile.groupFullName);
}
return !equal;
});
list.unshift(savedProfile);
@@ -527,11 +490,6 @@ export class ConnectionStore {
return result;
}
private loadProfiles(loadWorkspaceProfiles: boolean): IConnectionProfile[] {
let connections: IConnectionProfile[] = this._connectionConfig.getConnections(loadWorkspaceProfiles);
return connections;
}
private getMaxRecentConnectionsCount(): number {
let config = this._workspaceConfigurationService.getConfiguration(Constants.sqlConfigSectionName);

View File

@@ -25,3 +25,10 @@ export const mssqlProviderName = 'MSSQL';
export const applicationName = 'sqlops';
export const defaultEngine = 'defaultEngine';
export const passwordChars = '***************';
/* authentication types */
export const sqlLogin = 'SqlLogin';
export const integrated = 'Integrated';

View File

@@ -6,7 +6,7 @@
'use strict';
import { IConnectionManagementService, ConnectionOptionSpecialType } from 'sql/parts/connection/common/connectionManagement';
import { IConnectionComponentCallbacks, IConnectionComponentController, IConnectionResult } from 'sql/parts/connection/connectionDialog/connectionDialogService';
import { IConnectionComponentCallbacks, IConnectionComponentController, IConnectionValidateResult } from 'sql/parts/connection/connectionDialog/connectionDialogService';
import { ConnectionWidget } from 'sql/parts/connection/connectionDialog/connectionWidget';
import { AdvancedPropertiesController } from 'sql/parts/connection/connectionDialog/advancedPropertiesController';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
@@ -119,7 +119,7 @@ export class ConnectionController implements IConnectionComponentController {
this._connectionWidget.focusOnOpen();
}
public validateConnection(): IConnectionResult {
public validateConnection(): IConnectionValidateResult {
return { isValid: this._connectionWidget.connect(this._model), connection: this._model };
}

View File

@@ -7,7 +7,7 @@
import {
IConnectionDialogService, IConnectionManagementService, IErrorMessageService,
ConnectionType, INewConnectionParams, IConnectionCompletionOptions
ConnectionType, INewConnectionParams, IConnectionCompletionOptions, IConnectionResult
} from 'sql/parts/connection/common/connectionManagement';
import { ConnectionDialogWidget, OnShowUIResponse } from 'sql/parts/connection/connectionDialog/connectionDialogWidget';
import { ConnectionController } from 'sql/parts/connection/connectionDialog/connectionController';
@@ -24,10 +24,16 @@ import { IPartService } from 'vs/workbench/services/part/common/partService';
import { withElementById } from 'vs/base/browser/builder';
import { TPromise } from 'vs/base/common/winjs.base';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as platform from 'vs/base/common/platform';
import Severity from 'vs/base/common/severity';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { Action, IAction } from 'vs/base/common/actions';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import * as types from 'vs/base/common/types';
export interface IConnectionResult {
export interface IConnectionValidateResult {
isValid: boolean;
connection: IConnectionProfile;
}
@@ -42,7 +48,7 @@ export interface IConnectionComponentCallbacks {
export interface IConnectionComponentController {
showUiComponent(container: HTMLElement): void;
initDialog(model: IConnectionProfile): void;
validateConnection(): IConnectionResult;
validateConnection(): IConnectionValidateResult;
fillInConnectionInputs(connectionInfo: IConnectionProfile): void;
handleOnConnecting(): void;
handleResetConnection(): void;
@@ -65,14 +71,17 @@ export class ConnectionDialogService implements IConnectionDialogService {
private _providerTypes: string[];
private _currentProviderType: string = 'Microsoft SQL Server';
private _connecting: boolean = false;
private _connectionErrorTitle = localize('connectionError', 'Connection Error');
private _connectionErrorTitle = localize('connectionError', 'Connection error');
constructor(
@IPartService private _partService: IPartService,
@IInstantiationService private _instantiationService: IInstantiationService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
@IWindowsService private _windowsService: IWindowsService,
@IClipboardService private _clipboardService: IClipboardService,
@ICommandService private _commandService: ICommandService
) {
this._capabilitiesMaps = {};
this._providerNameToDisplayNameMap = {};
@@ -172,13 +181,13 @@ export class ConnectionDialogService implements IConnectionDialogService {
} else if (connectionResult && connectionResult.errorHandled) {
this._connectionDialog.resetConnection();
} else {
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage);
this._connectionDialog.resetConnection();
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack);
}
}).catch(err => {
this._connecting = false;
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, err);
this._connectionDialog.resetConnection();
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, err);
});
}
@@ -277,7 +286,7 @@ export class ConnectionDialogService implements IConnectionDialogService {
connectionManagementService: IConnectionManagementService,
params: INewConnectionParams,
model?: IConnectionProfile,
error?: string): Thenable<void> {
connectionResult?: IConnectionResult): Thenable<void> {
this._connectionManagementService = connectionManagementService;
this._params = params;
@@ -303,8 +312,8 @@ export class ConnectionDialogService implements IConnectionDialogService {
}
resolve(this.showDialogWithModel().then(() => {
if (error && error !== '') {
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, error);
if (connectionResult && connectionResult.errorMessage) {
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack);
}
}));
}, err => reject(err));
@@ -333,6 +342,33 @@ export class ConnectionDialogService implements IConnectionDialogService {
}
private getCurrentProviderName(): string {
return 'MSSQL';
return Object.keys(this._providerNameToDisplayNameMap).find(providerName => {
return this._currentProviderType === this._providerNameToDisplayNameMap[providerName];
});
}
private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string): void {
// Kerberos errors are currently very hard to understand, so adding handling of these to solve the common scenario
// note that ideally we would have an extensible service to handle errors by error code and provider, but for now
// this solves the most common "hard error" that we've noticed
const helpLink = 'https://aka.ms/sqlopskerberos';
let actions: IAction[] = [];
if (!platform.isWindows && types.isString(message) && message.toLowerCase().includes('kerberos') && message.toLowerCase().includes('kinit')) {
message = [
localize('kerberosErrorStart', "Connection failed due to Kerberos error."),
localize('kerberosHelpLink', "&nbsp;Help configuring Kerberos is available at ") + helpLink,
localize('kerberosKinit', "&nbsp;If you have previously connected you may need to re-run kinit.")
].join('<br/>');
actions.push(new Action('Kinit', 'Run kinit', null, true, () => {
this._connectionDialog.close();
this._clipboardService.writeText('kinit\r');
this._commandService.executeCommand('workbench.action.terminal.focus').then(resolve => {
return this._commandService.executeCommand('workbench.action.terminal.paste');
}).then(resolve => null, reject => null);
return null;
}));
}
this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions);
}
}

View File

@@ -4,7 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/connectionDialog';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import nls = require('vs/nls');
import { Button } from 'sql/base/browser/ui/button/button';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import { TPromise } from 'vs/base/common/winjs.base';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { Modal } from 'sql/base/browser/ui/modal/modal';
@@ -14,51 +17,37 @@ import { TreeCreationUtils } from 'sql/parts/registeredServer/viewlet/treeCreati
import { TreeUpdateUtils } from 'sql/parts/registeredServer/viewlet/treeUpdateUtils';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import * as styler from 'vs/platform/theme/common/styler';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import Event, { Emitter } from 'vs/base/common/event';
import { Builder, $ } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { RecentConnectionTreeController, RecentConnectionActionsProvider } from 'sql/parts/connection/connectionDialog/recentConnectionTreeController';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, IConfirmation } from 'vs/platform/message/common/message';
export interface OnShowUIResponse {
selectedProviderType: string;
container: HTMLElement;
}
class TreeController 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);
this.clickcb(tree.getSelection()[0], event, 'keyboard');
return true;
}
}
export class ConnectionDialogWidget extends Modal {
private _bodyBuilder: Builder;
private _recentConnectionBuilder: Builder;
private _noRecentConnectionBuilder: Builder;
private _dividerBuilder: Builder;
private _connectButton: Button;
private _closeButton: Button;
private _providerTypeSelectBox: SelectBox;
private _newConnectionParams: INewConnectionParams;
private _recentConnectionTree: ITree;
private $connectionUIContainer: Builder;
@@ -89,16 +78,20 @@ export class ConnectionDialogWidget extends Modal {
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
@IPartService _partService: IPartService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IMessageService private _messageService: IMessageService
) {
super(localize('connection', 'Connection'), TelemetryKeys.Connection, _partService, telemetryService, contextKeyService, { hasSpinner: true, hasErrors: true });
}
protected renderBody(container: HTMLElement): void {
this._bodyBuilder = new Builder(container);
this._providerTypeSelectBox = new SelectBox(this.providerTypeOptions, this.selectedProviderType);
this._bodyBuilder.div({ class: 'connection-recent', id: 'recentConnection' }, (builder) => {
this._recentConnectionBuilder = new Builder(builder.getHTMLElement());
this._noRecentConnectionBuilder = new Builder(builder.getHTMLElement());
this.createRecentConnections();
this._recentConnectionBuilder.hide();
});
@@ -108,16 +101,10 @@ export class ConnectionDialogWidget extends Modal {
});
this._bodyBuilder.div({ class: 'connection-type' }, (modelTableContent) => {
// add SQL Server label to Connection Dialog until we support multiple connection providers
let sqlServerName = localize('microsoftSqlServer', "Microsoft SQL Server");
modelTableContent.div({ class: 'server-name-label' }, (nameLabel) => {
nameLabel.innerHtml(sqlServerName);
});
//let connectTypeLabel = localize('connectType', 'Connection type');
let connectTypeLabel = localize('connectType', 'Connection type');
modelTableContent.element('table', { class: 'connection-table-content' }, (tableContainer) => {
// DialogHelper.appendInputSelectBox(
// DialogHelper.appendRow(tableContainer, connectTypeLabel, 'connection-label', 'connection-input'), this._providerTypeSelectBox);
DialogHelper.appendInputSelectBox(
DialogHelper.appendRow(tableContainer, connectTypeLabel, 'connection-label', 'connection-input'), this._providerTypeSelectBox);
});
});
@@ -141,7 +128,7 @@ export class ConnectionDialogWidget extends Modal {
this._connectButton.enabled = false;
this._closeButton = this.addFooterButton(cancelLabel, () => this.cancel());
this.registerListeners();
this.onProviderTypeSelected('MSSQL');
this.onProviderTypeSelected(this._providerTypeSelectBox.value);
}
// Update theming that is specific to connection flyout body
@@ -156,9 +143,14 @@ export class ConnectionDialogWidget extends Modal {
}
private registerListeners(): void {
this._register(styler.attachButtonStyler(this._connectButton, this._themeService));
this._register(styler.attachButtonStyler(this._closeButton, this._themeService));
// Theme styler
this._register(styler.attachSelectBoxStyler(this._providerTypeSelectBox, this._themeService));
this._register(attachButtonStyler(this._connectButton, this._themeService));
this._register(attachButtonStyler(this._closeButton, this._themeService));
this._register(this._providerTypeSelectBox.onDidSelect(selectedProviderType => {
this.onProviderTypeSelected(selectedProviderType.selected);
}));
}
private onProviderTypeSelected(selectedProviderType: string) {
@@ -171,6 +163,7 @@ export class ConnectionDialogWidget extends Modal {
private connect(element?: IConnectionProfile): void {
if (this._connectButton.enabled) {
this._connectButton.enabled = false;
this._providerTypeSelectBox.disable();
this.showSpinner();
this._onConnect.fire(element);
}
@@ -198,13 +191,37 @@ export class ConnectionDialogWidget extends Modal {
this.hide();
}
private createRecentConnections() {
private clearRecentConnectionList(): TPromise<boolean> {
let confirm: IConfirmation = {
message: nls.localize('clearRecentConnectionMessage', 'Are you sure you want to delete all the connections from the list?'),
primaryButton: localize('yes', 'Yes'),
secondaryButton: localize('no', 'No'),
type: 'question'
};
return this._messageService.confirm(confirm).then(confirmation => {
if (!confirmation.confirmed) {
return TPromise.as(false);
} else {
this._connectionManagementService.clearRecentConnectionsList();
this.open(false);
return TPromise.as(true);
}
});
}
private createRecentConnectionList(): void {
this._recentConnectionBuilder.div({ class: 'connection-recent-content' }, (recentConnectionContainer) => {
let recentHistoryLabel = localize('recentHistory', 'Recent history');
recentConnectionContainer.div({ class: 'connection-history-label' }, (recentTitle) => {
recentTitle.innerHtml(recentHistoryLabel);
recentConnectionContainer.div({ class: 'recent-titles-container'}, (container) => {
container.div({ class: 'connection-history-label' }, (recentTitle) => {
recentTitle.innerHtml(recentHistoryLabel);
});
container.div({ class: 'search-action clear-search-results'}, (clearSearchIcon) => {
clearSearchIcon.on('click', () => this.clearRecentConnectionList());
});
});
recentConnectionContainer.div({ class: 'server-explorer-viewlet' }, (divContainer: Builder) => {
divContainer.div({ class: 'explorer-servers' }, (treeContainer: Builder) => {
let leftClick = (element: any, eventish: ICancelableEvent, origin: string) => {
@@ -212,9 +229,16 @@ export class ConnectionDialogWidget extends Modal {
if (element instanceof ConnectionProfile) {
this.onRecentConnectionClick({ payload: { origin: origin, originalEvent: eventish } }, element);
}
};
let controller = new TreeController(leftClick);
let actionProvider = this._instantiationService.createInstance(RecentConnectionActionsProvider, this._instantiationService, this._connectionManagementService,
this._messageService);
let controller = new RecentConnectionTreeController(leftClick, actionProvider, this._connectionManagementService, this._contextMenuService);
actionProvider.onRecentConnectionRemoved(() => {
this.open(this._connectionManagementService.getRecentConnections().length > 0);
});
controller.onRecentConnectionRemoved(() => {
this.open(this._connectionManagementService.getRecentConnections().length > 0);
})
this._recentConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer.getHTMLElement(), this._instantiationService, controller);
// Theme styler
@@ -225,6 +249,20 @@ export class ConnectionDialogWidget extends Modal {
});
}
private createRecentConnections() {
this.createRecentConnectionList();
this._noRecentConnectionBuilder.div({ class: 'connection-recent-content' }, (noRecentConnectionContainer) => {
let recentHistoryLabel = localize('recentHistory', 'Recent history');
noRecentConnectionContainer.div({ class: 'connection-history-label' }, (recentTitle) => {
recentTitle.innerHtml(recentHistoryLabel);
});
let noRecentHistoryLabel = localize('noRecentConnections', 'No Recent Connections');
noRecentConnectionContainer.div({ class: 'no-recent-connections' }, (noRecentTitle) => {
noRecentTitle.innerHtml(noRecentHistoryLabel);
});
});
}
private onRecentConnectionClick(event: any, element: IConnectionProfile) {
let isMouseOrigin = event.payload && (event.payload.origin === 'mouse');
let isDoubleClick = isMouseOrigin && event.payload.originalEvent && event.payload.originalEvent.detail === 2;
@@ -244,14 +282,15 @@ export class ConnectionDialogWidget extends Modal {
public open(recentConnections: boolean) {
this.show();
if (recentConnections) {
this._noRecentConnectionBuilder.hide();
this._recentConnectionBuilder.show();
TreeUpdateUtils.structuralTreeUpdate(this._recentConnectionTree, 'recent', this._connectionManagementService);
// call layout with view height
this.layout();
} else {
this._recentConnectionBuilder.hide();
this._noRecentConnectionBuilder.show();
}
TreeUpdateUtils.structuralTreeUpdate(this._recentConnectionTree, 'recent', this._connectionManagementService);
// call layout with view height
this.layout();
this.initDialog();
}
@@ -284,6 +323,7 @@ export class ConnectionDialogWidget extends Modal {
public resetConnection(): void {
this.hideSpinner();
this._connectButton.enabled = true;
this._providerTypeSelectBox.enable();
this._onResetConnection.fire();
}
@@ -296,6 +336,7 @@ export class ConnectionDialogWidget extends Modal {
}
public updateProvider(displayName: string) {
this.onProviderTypeSelected('MSSQL');
this._providerTypeSelectBox.selectWithOptionName(displayName);
this.onProviderTypeSelected(displayName);
}
}

View File

@@ -7,10 +7,10 @@
import 'vs/css!./media/sqlConnection';
import { Builder, $ } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { Button } from 'sql/base/browser/ui/button/button';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { Checkbox } from 'sql/base/browser/ui/checkbox/defaultCheckbox';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { IConnectionComponentCallbacks } from 'sql/parts/connection/connectionDialog/connectionDialogService';
@@ -21,11 +21,12 @@ import * as Constants from 'sql/parts/connection/common/constants';
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import * as styler from 'vs/platform/theme/common/styler';
import { attachInputBoxStyler } from 'sql/common/theme/styler';
import { attachInputBoxStyler, attachButtonStyler } from 'sql/common/theme/styler';
import * as DOM from 'vs/base/browser/dom';
import data = require('data');
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { localize } from 'vs/nls';
import { OS, OperatingSystem } from 'vs/base/common/platform';
export class ConnectionWidget {
private _builder: Builder;
@@ -36,6 +37,7 @@ export class ConnectionWidget {
private _databaseNameInputBox: InputBox;
private _userNameInputBox: InputBox;
private _passwordInputBox: InputBox;
private _password: string;
private _rememberPasswordCheckBox: Checkbox;
private _advancedButton: Button;
private _callbacks: IConnectionComponentCallbacks;
@@ -43,9 +45,10 @@ export class ConnectionWidget {
private _toDispose: lifecycle.IDisposable[];
private _optionsMaps: { [optionType: number]: data.ConnectionOption };
private _tableContainer: Builder;
private _focusedBeforeHandleOnConnection: HTMLElement;
private _providerName: string;
private _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
[Constants.mssqlProviderName]: [new AuthenticationType('Integrated', false), new AuthenticationType('SqlLogin', true)]
[Constants.mssqlProviderName]: [new AuthenticationType(Constants.integrated, false), new AuthenticationType(Constants.sqlLogin, true)]
};
private _saveProfile: boolean;
public DefaultServerGroup: IConnectionProfileGroup = {
@@ -84,7 +87,14 @@ export class ConnectionWidget {
}
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
this._authTypeSelectBox = authTypeOption ? new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue) : undefined;
if(authTypeOption) {
if (OS === OperatingSystem.Windows) {
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.integrated);
} else {
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.sqlLogin);
}
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue);
}
this._providerName = providerName;
}
@@ -134,6 +144,7 @@ export class ConnectionWidget {
let passwordBuilder = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input');
this._passwordInputBox = new InputBox(passwordBuilder.getHTMLElement(), this._contextViewService);
this._passwordInputBox.inputElement.type = 'password';
this._password = '';
let rememberPasswordLabel = localize('rememberPassword', 'Remember password');
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', false);
@@ -201,13 +212,14 @@ export class ConnectionWidget {
this._toDispose.push(attachInputBoxStyler(this._userNameInputBox, this._themeService));
this._toDispose.push(attachInputBoxStyler(this._passwordInputBox, this._themeService));
this._toDispose.push(styler.attachSelectBoxStyler(this._serverGroupSelectBox, this._themeService));
this._toDispose.push(styler.attachButtonStyler(this._advancedButton, this._themeService));
this._toDispose.push(attachButtonStyler(this._advancedButton, this._themeService));
if (this._authTypeSelectBox) {
// Theme styler
this._toDispose.push(styler.attachSelectBoxStyler(this._authTypeSelectBox, this._themeService));
this._toDispose.push(this._authTypeSelectBox.onDidSelect(selectedAuthType => {
this.onAuthTypeSelected(selectedAuthType.selected);
this.setConnectButton();
}));
}
@@ -218,6 +230,14 @@ export class ConnectionWidget {
this._toDispose.push(this._serverNameInputBox.onDidChange(serverName => {
this.serverNameChanged(serverName);
}));
this._toDispose.push(this._userNameInputBox.onDidChange(userName => {
this.setConnectButton();
}));
this._toDispose.push(this._passwordInputBox.onDidChange(passwordInput => {
this._password = passwordInput;
}));
}
private onGroupSelected(selectedGroup: string) {
@@ -230,6 +250,17 @@ export class ConnectionWidget {
}
}
private setConnectButton() : void {
let authDisplayName: string = this.getAuthTypeDisplayName(this.authenticationType);
let authType: AuthenticationType = this.getMatchingAuthType(authDisplayName);
let showUsernameAndPassword: boolean = true;
if(authType) {
showUsernameAndPassword = authType.showUsernameAndPassword;
}
showUsernameAndPassword ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) :
this._callbacks.onSetConnectButton(!!this.serverName);
}
private onAuthTypeSelected(selectedAuthType: string) {
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
if (!currentAuthType.showUsernameAndPassword) {
@@ -239,6 +270,7 @@ export class ConnectionWidget {
this._passwordInputBox.hideMessage();
this._userNameInputBox.value = '';
this._passwordInputBox.value = '';
this._password = '';
this._rememberPasswordCheckBox.checked = false;
this._rememberPasswordCheckBox.enabled = false;
@@ -250,7 +282,7 @@ export class ConnectionWidget {
}
private serverNameChanged(serverName: string) {
this._callbacks.onSetConnectButton(!!serverName);
this.setConnectButton();
if (serverName.toLocaleLowerCase().includes('database.windows.net')) {
this._callbacks.onSetAzureTimeOut();
}
@@ -289,10 +321,10 @@ export class ConnectionWidget {
public fillInConnectionInputs(connectionInfo: IConnectionProfile) {
if (connectionInfo) {
this._serverNameInputBox.value = this.getModelValue(connectionInfo.serverName);
this._callbacks.onSetConnectButton(!!connectionInfo.serverName);
this._databaseNameInputBox.value = this.getModelValue(connectionInfo.databaseName);
this._userNameInputBox.value = this.getModelValue(connectionInfo.userName);
this._passwordInputBox.value = this.getModelValue(connectionInfo.password);
this._passwordInputBox.value = connectionInfo.password ? Constants.passwordChars : '';
this._password = this.getModelValue(connectionInfo.password);
this._saveProfile = connectionInfo.saveProfile;
let groupName: string;
if (this._saveProfile) {
@@ -321,18 +353,26 @@ export class ConnectionWidget {
if (this._authTypeSelectBox) {
this.onAuthTypeSelected(this._authTypeSelectBox.value);
}
// Disable connect button if -
// 1. Authentication type is SQL Login and no username is provided
// 2. No server name is provided
this.setConnectButton();
}
}
private getAuthTypeDisplayName(authTypeName: string) {
var displayName: string;
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
authTypeOption.categoryValues.forEach(c => {
if (c.name === authTypeName) {
displayName = c.displayName;
}
});
if(authTypeOption) {
authTypeOption.categoryValues.forEach(c => {
if (c.name === authTypeName) {
displayName = c.displayName;
}
});
}
return displayName;
}
@@ -348,6 +388,7 @@ export class ConnectionWidget {
}
public handleOnConnecting(): void {
this._focusedBeforeHandleOnConnection = <HTMLElement>document.activeElement;
this._advancedButton.enabled = false;
this._serverGroupSelectBox.disable();
@@ -378,6 +419,10 @@ export class ConnectionWidget {
this._passwordInputBox.enable();
this._rememberPasswordCheckBox.enabled = true;
}
if (this._focusedBeforeHandleOnConnection) {
this._focusedBeforeHandleOnConnection.focus();
}
}
public get serverName(): string {
@@ -393,7 +438,7 @@ export class ConnectionWidget {
}
public get password(): string {
return this._passwordInputBox.value;
return this._password;
}
public get authenticationType(): string {
@@ -467,7 +512,8 @@ export class ConnectionWidget {
}
private getMatchingAuthType(displayName: string): AuthenticationType {
return this._authTypeMap[this._providerName].find(authType => this.getAuthTypeDisplayName(authType.name) === displayName);
const authType = this._authTypeMap[this._providerName];
return authType ? authType.find(authType => this.getAuthTypeDisplayName(authType.name) === displayName) : undefined;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#1E1E1E" d="M4.222 0h-2.222v.479c-.526.648-.557 1.57-.043 2.269l.043.059v3.203l-.4.296-.053.053c-.353.352-.547.822-.547 1.321s.194.967.549 1.32c.134.134.288.236.451.322v6.678h14v-16h-11.778z"/><path fill="#E8E8E8" d="M10.798 7l-1.83-2h6.032v2h-4.202zm-2.292-6h-3.207l1.337 1.52 1.87-1.52zm-5.506 8.531v1.469h12v-2h-10.813l-.024.021c-.3.299-.716.479-1.163.51zm0 5.469h12v-2h-12v2zm3.323-8h.631l-.347-.266-.284.266zm8.677-4v-2h-3.289l-1.743 2h5.032z"/><path fill="#F48771" d="M7.246 4.6l2.856-3.277-.405-.002-3.176 2.581-2.607-2.962c-.336-.221-.786-.2-1.082.096-.308.306-.319.779-.069 1.12l2.83 2.444-3.339 2.466c-.339.338-.339.887 0 1.225.339.337.888.337 1.226 0l3.063-2.867 3.33 2.555h.466l-3.093-3.379z"/></svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M4.222 0h-2.222v.479c-.526.648-.557 1.57-.043 2.269l.043.059v3.203l-.4.296-.053.053c-.353.352-.547.822-.547 1.321s.194.967.549 1.32c.134.134.288.236.451.322v6.678h14v-16h-11.778z"/><path fill="#424242" d="M10.798 7l-1.83-2h6.032v2h-4.202zm-2.292-6h-3.207l1.337 1.52 1.87-1.52zm-5.506 8.531v1.469h12v-2h-10.813l-.024.021c-.3.299-.716.479-1.163.51zm0 5.469h12v-2h-12v2zm3.323-8h.631l-.347-.266-.284.266zm8.677-4v-2h-3.289l-1.743 2h5.032z"/><path fill="#A1260D" d="M7.246 4.6l2.856-3.277-.405-.002-3.176 2.581-2.607-2.962c-.336-.221-.786-.2-1.082.096-.308.306-.319.779-.069 1.12l2.83 2.444-3.339 2.466c-.339.338-.339.887 0 1.225.339.337.888.337 1.226 0l3.063-2.867 3.33 2.555h.466l-3.093-3.379z"/></svg>

After

Width:  |  Height:  |  Size: 784 B

View File

@@ -20,13 +20,22 @@
overflow-y: auto;
}
.connection-history-label {
font-size: 15px;
.no-recent-connections {
font-size: 12px;
text-align: left;
display: block;
padding-top: 5px;
}
.server-name-label {
.connection-history-label {
font-size: 15px;
margin-bottom: 15px;
display: inline;
text-align: left;
}
.recent-titles-container {
display: flex;
justify-content: space-between;
}
.connection-provider-info {
@@ -46,4 +55,17 @@
.connection-type {
margin: 15px;
overflow-y: hidden;
}
.search-action.clear-search-results {
background: url('clear-search-results.svg') center right no-repeat;
width: 10%;
cursor: pointer;
}
.vs-dark .search-action.clear-search-results,
.hc-black .search-action.clear-search-results {
background: url('clear-search-results-dark.svg') center right no-repeat;
width: 10%;
cursor: pointer;
}

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
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/parts/connection/common/connectionActions';
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IAction } from 'vs/base/common/actions';
import Event, { Emitter } from 'vs/base/common/event';
import { IMessageService } from 'vs/platform/message/common/message';
import mouse = require('vs/base/browser/mouseEvent');
import { IConnectionProfile } from 'sql/parts/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,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IMessageService private _messageService: IMessageService,
) {
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): TPromise<IAction[]> {
if (element instanceof ConnectionProfile) {
return TPromise.as(this.getRecentConnectionActions(tree, element));
}
return TPromise.as([]);
}
}
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.onContextMenu(tree, element, event);
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 onContextMenu(tree: ITree, element: any, event: any): boolean {
var 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

@@ -2,28 +2,112 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { Action, IAction } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IAngularEventingService, AngularEventType } from 'sql/services/angularEventing/angularEventingService';
export class RefreshWidgetAction extends Action {
export class EditDashboardAction extends Action {
public static ID = 'refreshWidget';
public static LABEL = nls.localize('refreshWidget', 'Refresh');
private static readonly ID = 'editDashboard';
private static readonly EDITLABEL = nls.localize('editDashboard', "Edit");
private static readonly EXITLABEL = nls.localize('editDashboardExit', "Exit");
private static readonly ICON = 'edit';
private _state = 0;
constructor(
id: string, label: string,
private refreshFn: () => void
private editFn: () => void,
private context: any //this
) {
super(id, label);
super(EditDashboardAction.ID, EditDashboardAction.EDITLABEL, EditDashboardAction.ICON);
}
run(): TPromise<boolean> {
try {
this.refreshFn();
this.editFn.apply(this.context);
this.toggleLabel();
return TPromise.as(true);
} catch (e) {
return TPromise.as(false);
}
}
private toggleLabel(): void {
if (this._state === 0) {
this.label = EditDashboardAction.EXITLABEL;
this._state = 1;
} else {
this.label = EditDashboardAction.EDITLABEL;
this._state = 0;
}
}
}
export class RefreshWidgetAction extends Action {
private static readonly ID = 'refreshWidget';
private static readonly LABEL = nls.localize('refreshWidget', 'Refresh');
private static readonly ICON = 'refresh';
constructor(
private refreshFn: () => void,
private context: any // this
) {
super(RefreshWidgetAction.ID, RefreshWidgetAction.LABEL, RefreshWidgetAction.ICON);
}
run(): TPromise<boolean> {
try {
this.refreshFn.apply(this.context);
return TPromise.as(true);
} catch (e) {
return TPromise.as(false);
}
}
}
export class ToggleMoreWidgetAction extends Action {
private static readonly ID = 'toggleMore';
private static readonly LABEL = nls.localize('toggleMore', 'Toggle More');
private static readonly ICON = 'toggle-more';
constructor(
private _actions: Array<IAction>,
private _context: any,
@IContextMenuService private _contextMenuService: IContextMenuService
) {
super(ToggleMoreWidgetAction.ID, ToggleMoreWidgetAction.LABEL, ToggleMoreWidgetAction.ICON);
}
run(context: StandardKeyboardEvent): TPromise<boolean> {
this._contextMenuService.showContextMenu({
getAnchor: () => context.target,
getActions: () => TPromise.as(this._actions),
getActionsContext: () => this._context
});
return TPromise.as(true);
}
}
export class DeleteWidgetAction extends Action {
private static readonly ID = 'deleteWidget';
private static readonly LABEL = nls.localize('deleteWidget', "Delete Widget");
private static readonly ICON = 'close';
constructor(
private _widgetId,
private _uri,
@IAngularEventingService private angularEventService: IAngularEventingService
) {
super(DeleteWidgetAction.ID, DeleteWidgetAction.LABEL, DeleteWidgetAction.ICON);
}
run(): TPromise<boolean> {
this.angularEventService.sendAngularEvent(this._uri, AngularEventType.DELETE_WIDGET, { id: this._widgetId });
return TPromise.as(true);
}
}

View File

@@ -4,13 +4,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div #propertyContainer>
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="margin-left: 10px; margin-right: 10px; height: 90px; display: block">
</dashboard-widget-wrapper>
</div>
<div>
<div [ngGrid]="gridConfig">
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
</dashboard-widget-wrapper>
<div #scrollContainer style="height: 100%">
<div #scrollable style="position: relative">
<div #propertiesContainer>
<dashboard-widget-wrapper #properties *ngIf="propertiesWidget" [_config]="propertiesWidget" style="padding-left: 10px; padding-right: 10px; height: 90px; display: block">
</dashboard-widget-wrapper>
</div>
<div [ngGrid]="gridConfig">
<dashboard-widget-wrapper *ngFor="let widget of widgets" [(ngGridItem)]="widget.gridItemConfig" [_config]="widget">
</dashboard-widget-wrapper>
</div>
</div>
</div>
</div>

View File

@@ -3,22 +3,33 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList } from '@angular/core';
import { NgGridConfig } from 'angular2-grid';
import 'vs/css!./dashboardPage';
import { Component, Inject, forwardRef, ViewChild, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { NgGridConfig, NgGrid, NgGridItem } from 'angular2-grid';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
import { subscriptionToDisposable } from 'sql/base/common/lifecycle';
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
import { Registry } from 'vs/platform/registry/common/platform';
import * as types from 'vs/base/common/types';
import { Severity } from 'vs/platform/message/common/message';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { addDisposableListener, getContentHeight, EventType } from 'vs/base/browser/dom';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as nls from 'vs/nls';
import * as themeColors from 'vs/workbench/common/theme';
import { generateUuid } from 'vs/base/common/uuid';
import * as objects from 'vs/base/common/objects';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
/**
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
@@ -27,14 +38,56 @@ function isNumberArray(value: any): value is number[] {
return types.isArray(value) && (<any[]>value).every(elem => types.isNumber(elem));
}
/**
* Sorting function for dashboard widgets
* In order of priority;
* If neither have defined grid positions, they are equivalent
* If a has a defined grid position and b does not; a should come first
* If both have defined grid positions and have the same row; the one with the smaller col position should come first
* If both have defined grid positions but different rows (it doesn't really matter in this case) the lowers row should come first
*/
function configSorter(a, b): number {
if ((!a.gridItemConfig || !a.gridItemConfig.col)
&& (!b.gridItemConfig || !b.gridItemConfig.col)) {
return 0;
} else if (!a.gridItemConfig || !a.gridItemConfig.col) {
return 1;
} else if (!b.gridItemConfig || !b.gridItemConfig.col) {
return -1;
} else if (a.gridItemConfig.row === b.gridItemConfig.row) {
if (a.gridItemConfig.col < b.gridItemConfig.col) {
return -1;
}
if (a.gridItemConfig.col === b.gridItemConfig.col) {
return 0;
}
if (a.gridItemConfig.col > b.gridItemConfig.col) {
return 1;
}
} else {
if (a.gridItemConfig.row < b.gridItemConfig.row) {
return -1;
}
if (a.gridItemConfig.row === b.gridItemConfig.row) {
return 0;
}
if (a.gridItemConfig.row > b.gridItemConfig.row) {
return 1;
}
}
return void 0; // this should never be reached
}
@Component({
selector: 'dashboard-page',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html')),
host: {
class: 'dashboard-page'
}
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html'))
})
export abstract class DashboardPage {
export abstract class DashboardPage extends Disposable implements OnDestroy {
protected SKELETON_WIDTH = 5;
protected widgets: Array<WidgetConfig> = [];
@@ -60,55 +113,124 @@ export abstract class DashboardPage {
'prefer_new': false, // When adding new items, will use that items position ahead of existing items
'limit_to_screen': true, // When resizing the screen, with this true and auto_resize false, the grid will re-arrange to fit the screen size. Please note, at present this only works with cascade direction up.
};
private _themeDispose: IDisposable;
private _originalConfig: WidgetConfig[];
private _editDispose: Array<IDisposable> = [];
private _scrollableElement: ScrollableElement;
private _widgetConfigLocation: string;
private _propertiesConfigLocation: string;
@ViewChild('propertyContainer', { read: ElementRef }) private propertyContainer: ElementRef;
@ViewChild('properties') private _properties: DashboardWidgetWrapper;
@ViewChild(NgGrid) private _grid: NgGrid;
@ViewChild('scrollable', { read: ElementRef }) private _scrollable: ElementRef;
@ViewChild('scrollContainer', { read: ElementRef }) private _scrollContainer: ElementRef;
@ViewChild('propertiesContainer', { read: ElementRef }) private _propertiesContainer: ElementRef;
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
@ViewChildren(NgGridItem) private _items: QueryList<NgGridItem>;
// a set of config modifiers
private readonly _configModifiers: Array<(item: Array<WidgetConfig>) => Array<WidgetConfig>> = [
this.removeEmpty,
this.initExtensionConfigs,
this.validateGridConfig,
this.addProvider,
this.addEdition,
this.addContext,
this.filterWidgets
];
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>) => Array<WidgetConfig>> = [
this.validateGridConfig
];
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface
) { }
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
) {
super();
}
protected init() {
if (!this.dashboardService.connectionManagementService.connectionInfo) {
this.dashboardService.messageService.show(Severity.Warning, nls.localize('missingConnectionInfo', 'No connection information could be found for this dashboard'));
} else {
let tempWidgets = this.dashboardService.getSettings(this.context).widgets;
let tempWidgets = this.dashboardService.getSettings<Array<WidgetConfig>>([this.context, 'widgets'].join('.'));
this._widgetConfigLocation = 'default';
this._originalConfig = objects.clone(tempWidgets);
let properties = this.getProperties();
this._configModifiers.forEach((cb) => {
tempWidgets = cb.apply(this, [tempWidgets]);
properties = properties ? cb.apply(this, [properties]) : undefined;
});
this._gridModifiers.forEach(cb => {
tempWidgets = cb.apply(this, [tempWidgets]);
});
this.widgets = tempWidgets;
this.propertiesWidget = properties ? properties[0] : undefined;
}
}
protected baseInit(): void {
let self = this;
self._themeDispose = self.dashboardService.themeService.onDidColorThemeChange((event: IColorTheme) => {
self.updateTheme(event);
ngAfterViewInit(): void {
this._register(this.dashboardService.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.dashboardService.themeService.getColorTheme());
let container = this._scrollContainer.nativeElement as HTMLElement;
let scrollable = this._scrollable.nativeElement as HTMLElement;
container.removeChild(scrollable);
this._scrollableElement = new ScrollableElement(scrollable, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false
});
self.updateTheme(self.dashboardService.themeService.getColorTheme());
this._scrollableElement.onScroll(e => {
scrollable.style.bottom = e.scrollTop + 'px';
});
container.appendChild(this._scrollableElement.getDomNode());
let initalHeight = getContentHeight(scrollable);
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
this._register(addDisposableListener(window, EventType.RESIZE, () => {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
}));
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
setTimeout(() => {
let currentheight = getContentHeight(scrollable);
if (initalHeight !== currentheight) {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
}
}, 100);
}
private updateTheme(theme: IColorTheme): void {
let el = this._propertiesContainer.nativeElement as HTMLElement;
let border = theme.getColor(colors.contrastBorder, true);
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
if (border) {
el.style.borderColor = border.toString();
el.style.borderBottomWidth = '1px';
el.style.borderBottomStyle = 'solid';
} else if (borderColor) {
el.style.borderBottom = '1px solid ' + borderColor.toString();
} else {
el.style.border = 'none';
}
}
protected baseDestroy(): void {
if (this._themeDispose) {
this._themeDispose.dispose();
}
ngOnDestroy() {
this.dispose();
}
protected abstract propertiesWidget: WidgetConfig;
@@ -215,10 +337,14 @@ export abstract class DashboardPage {
* @param config Array of widgets to validate
*/
protected validateGridConfig(config: WidgetConfig[]): Array<WidgetConfig> {
return config.map((widget) => {
return config.map((widget, index) => {
if (widget.gridItemConfig === undefined) {
widget.gridItemConfig = {};
}
const id = generateUuid();
widget.gridItemConfig.payload = { id };
widget.id = id;
this._originalConfig[index].id = id;
return widget;
});
}
@@ -246,6 +372,12 @@ export abstract class DashboardPage {
sizey: insightConfig.gridItemConfig.y
};
}
if (config.gridItemConfig && !config.gridItemConfig.sizex && insightConfig.gridItemConfig && insightConfig.gridItemConfig.x) {
config.gridItemConfig.sizex = insightConfig.gridItemConfig.x;
}
if (config.gridItemConfig && !config.gridItemConfig.sizey && insightConfig.gridItemConfig && insightConfig.gridItemConfig.y) {
config.gridItemConfig.sizey = insightConfig.gridItemConfig.y;
}
}
}
return config;
@@ -253,7 +385,8 @@ export abstract class DashboardPage {
}
private getProperties(): Array<WidgetConfig> {
let properties = this.dashboardService.getSettings(this.context).properties;
let properties = this.dashboardService.getSettings<IPropertiesConfig[]>([this.context, 'properties'].join('.'));
this._propertiesConfigLocation = 'default';
if (types.isUndefinedOrNull(properties)) {
return [this.propertiesWidget];
} else if (types.isBoolean(properties)) {
@@ -271,18 +404,6 @@ export abstract class DashboardPage {
}
}
private updateTheme(theme: IColorTheme): void {
let propsEl: HTMLElement = this.propertyContainer.nativeElement;
let widgetShadowColor = theme.getColor(colors.widgetShadow);
if (widgetShadowColor) {
// Box shadow on bottom only.
// The below settings fill the shadow across the whole page
propsEl.style.boxShadow = `-5px 5px 10px -5px ${widgetShadowColor}`;
propsEl.style.marginRight = '-10px';
propsEl.style.marginBottom = '5px';
}
}
public refresh(refreshConfig: boolean = false): void {
if (refreshConfig) {
this.init();
@@ -297,4 +418,85 @@ export abstract class DashboardPage {
}
}
}
public enableEdit(): void {
if (this._grid.dragEnable) {
this._grid.disableDrag();
this._grid.disableResize();
this._editDispose.forEach(i => i.dispose());
this._widgets.forEach(i => {
if (i.id) {
i.disableEdit();
}
});
this._editDispose = [];
} else {
this._grid.enableResize();
this._grid.enableDrag();
this._editDispose.push(this.dashboardService.onDeleteWidget(e => {
let index = this.widgets.findIndex(i => i.id === e);
this.widgets.splice(index, 1);
index = this._originalConfig.findIndex(i => i.id === e);
this._originalConfig.splice(index, 1);
this._rewriteConfig();
this._cd.detectChanges();
}));
this._editDispose.push(subscriptionToDisposable(this._grid.onResizeStop.subscribe((e: NgGridItem) => {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this._scrollable.nativeElement),
height: getContentHeight(this._scrollContainer.nativeElement)
});
let event = e.getEventOutput();
let config = this._originalConfig.find(i => i.id === event.payload.id);
if (!config.gridItemConfig) {
config.gridItemConfig = {};
}
config.gridItemConfig.sizex = e.sizex;
config.gridItemConfig.sizey = e.sizey;
let component = this._widgets.find(i => i.id === event.payload.id);
component.layout();
this._rewriteConfig();
})));
this._editDispose.push(subscriptionToDisposable(this._grid.onDragStop.subscribe((e: NgGridItem) => {
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this._scrollable.nativeElement),
height: getContentHeight(this._scrollContainer.nativeElement)
});
let event = e.getEventOutput();
this._items.forEach(i => {
let config = this._originalConfig.find(j => j.id === i.getEventOutput().payload.id);
if ((config.gridItemConfig && config.gridItemConfig.col) || config.id === event.payload.id) {
if (!config.gridItemConfig) {
config.gridItemConfig = {};
}
config.gridItemConfig.col = i.col;
config.gridItemConfig.row = i.row;
}
});
this._originalConfig.sort(configSorter);
this._rewriteConfig();
})));
this._widgets.forEach(i => {
if (i.id) {
i.enableEdit();
}
});
}
}
private _rewriteConfig(): void {
let writeableConfig = objects.clone(this._originalConfig);
writeableConfig.forEach(i => {
delete i.id;
});
let target: ConfigurationTarget = ConfigurationTarget.USER;
this.dashboardService.writeSettings(this.context, writeableConfig, target);
}
}

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-page {
height: 100%;
width: 100%;
position: absolute;
}

View File

@@ -2,19 +2,22 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { InjectionToken } from '@angular/core';
import { InjectionToken, OnDestroy } from '@angular/core';
import { NgGridItemConfig } from 'angular2-grid';
import { Action } from 'vs/base/common/actions';
import { Disposable } from 'vs/base/common/lifecycle';
export interface IDashboardWidget {
actions: Array<Action>;
actionsContext?: any;
refresh?: () => void;
layout?: () => void;
}
export const WIDGET_CONFIG = new InjectionToken<WidgetConfig>('widget_config');
export interface WidgetConfig {
id?: string; // used to track the widget lifespan operations
name?: string;
icon?: string;
context: string;
@@ -26,13 +29,17 @@ export interface WidgetConfig {
border?: string;
fontSize?: string;
fontWeight?: string;
padding?:string;
padding?: string;
}
export abstract class DashboardWidget {
export abstract class DashboardWidget extends Disposable implements OnDestroy {
protected _config: WidgetConfig;
get actions(): Array<Action> {
return [];
}
}
ngOnDestroy() {
this.dispose();
}
}

View File

@@ -7,12 +7,12 @@
<div style="display: flex; flex-flow: column; overflow: hidden; height: 100%; width: 100%">
<div #header>
<div *ngIf="_config.name || _config.loadedIcon || _actions" style="display: flex;flex: 0 0; padding: 3px 0 3px 0">
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
<div *ngIf="_config.name || _config.loadedIcon || _actions" style="display: flex; flex: 0 0; padding: 3px 0 3px 0; flex-direction: row-reverse; justify-content: space-between">
<span #actionbar style="flex: 0 0 auto; align-self: end"></span>
<span *ngIf="_config.name" style="margin-left: 5px">{{_config.name}}</span>
<div *ngIf="_actions" (click)="onActionsClick($event)" style="float: right; margin-right: 5px; margin-left: auto; padding: 10px" class="icon toggle-more"></div>
<span *ngIf="_config.icon" [ngClass]="['icon', _config.icon]" style="display: inline-block; padding: 10px; margin-left: 5px"></span>
</div>
</div>
<ng-template component-host>
</ng-template>
</div>
</div>

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!./dashboardWidgetWrapper';
import {
Component, Input, Inject, forwardRef, ComponentFactoryResolver, AfterContentInit, ViewChild,
@@ -13,7 +14,7 @@ import { ComponentHostDirective } from './componentHost.directive';
import { WidgetConfig, WIDGET_CONFIG, IDashboardWidget } from './dashboardWidget';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { error } from 'sql/base/common/log';
import * as ACTIONS from './actions';
import { RefreshWidgetAction, ToggleMoreWidgetAction, DeleteWidgetAction } from './actions';
/* Widgets */
import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
@@ -28,8 +29,8 @@ import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeS
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as themeColors from 'vs/workbench/common/theme';
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { Registry } from 'vs/platform/registry/common/platform';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
const componentMap: { [x: string]: Type<IDashboardWidget> } = {
'properties-widget': PropertiesWidgetComponent,
@@ -47,8 +48,10 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
private _themeDispose: IDisposable;
private _actions: Array<Action>;
private _component: IDashboardWidget;
private _actionbar: ActionBar;
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
@ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
constructor(
@@ -69,6 +72,11 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
ngAfterContentInit() {
this.updateTheme(this._bootstrap.themeService.getColorTheme());
this.loadWidget();
this._changeref.detectChanges();
this._actionbar = new ActionBar(this._actionbarRef.nativeElement);
if (this._actions) {
this._actionbar.push(this._bootstrap.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, this._component.actionsContext), { icon: true, label: false });
}
}
ngOnDestroy() {
@@ -81,6 +89,24 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
}
}
public layout(): void {
if (this._component && this._component.layout) {
this._component.layout();
}
}
public get id(): string {
return this._config.id;
}
public enableEdit(): void {
this._actionbar.push(this._bootstrap.instantiationService.createInstance(DeleteWidgetAction, this._config.id, this._bootstrap.getUnderlyingUri()), { icon: true, label: false });
}
public disableEdit(): void {
this._actionbar.pull(this._actionbar.length() - 1);
}
private loadWidget(): void {
if (Object.keys(this._config.widget).length !== 1) {
error('Exactly 1 widget must be defined per space');
@@ -105,7 +131,7 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
this._component = componentRef.instance;
let actions = componentRef.instance.actions;
if (componentRef.instance.refresh) {
actions.push(this._bootstrap.instantiationService.createInstance(ACTIONS.RefreshWidgetAction, ACTIONS.RefreshWidgetAction.ID, ACTIONS.RefreshWidgetAction.LABEL, componentRef.instance.refresh));
actions.push(new RefreshWidgetAction(componentRef.instance.refresh, componentRef.instance));
}
if (actions !== undefined && actions.length > 0) {
this._actions = actions;
@@ -148,23 +174,12 @@ export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestr
return selector;
}
//tslint:disable-next-line
private onActionsClick(e: any) {
let anchor = { x: e.pageX + 1, y: e.pageY };
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => TPromise.as(this._actions),
getActionsContext: () => this._component.actionsContext
});
}
private updateTheme(theme: IColorTheme): void {
let el = <HTMLElement>this._ref.nativeElement;
let headerEl: HTMLElement = this.header.nativeElement;
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
let backgroundColor = theme.getColor(colors.editorBackground, true);
let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
// TODO: highContrastBorder does not exist, how to handle?
let border = theme.getColor(colors.contrastBorder, true);
if (this._config.background_color) {

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.
*--------------------------------------------------------------------------------------------*/
dashboard-widget-wrapper .action-label {
padding: 7px;
}

View File

@@ -4,9 +4,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: scroll; width: 100%; height: 100%">
<div #header style="margin-bottom: 5px">
<breadcrumb></breadcrumb>
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #header class="header" style="margin-bottom: 5px; flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center">
<div style="flex: 1 1 auto">
<breadcrumb></breadcrumb>
</div>
<div style="flex: 0 0 auto" #actionBar>
</div>
</div>
<router-outlet></router-outlet>
</div>
<div style="flex: 1 1 auto; position: relative">
<router-outlet (activate)="onActivate($event)"></router-outlet>
</div>
</div>

View File

@@ -3,16 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dashboard';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { DashboardServiceInterface } from './services/dashboardServiceInterface.service';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import * as Utils from 'sql/parts/connection/common/utils';
import { RefreshWidgetAction, EditDashboardAction } from 'sql/parts/dashboard/common/actions';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as themeColors from 'vs/workbench/common/theme';
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
export const DASHBOARD_SELECTOR: string = 'dashboard-component';
@@ -22,7 +27,11 @@ export const DASHBOARD_SELECTOR: string = 'dashboard-component';
})
export class DashboardComponent implements OnInit, OnDestroy {
private _subs: Array<IDisposable> = new Array();
private _currentPage: DashboardPage;
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
@ViewChild('actionBar', { read: ElementRef }) private actionbarContainer: ElementRef;
private actionbar: ActionBar;
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrapService: DashboardServiceInterface,
@@ -31,13 +40,21 @@ export class DashboardComponent implements OnInit, OnDestroy {
) { }
ngOnInit() {
let self = this;
self._subs.push(self._bootstrapService.themeService.onDidColorThemeChange(e => self.updateTheme(e)));
self.updateTheme(self._bootstrapService.themeService.getColorTheme());
let profile: IConnectionProfile = self._bootstrapService.getOriginalConnectionProfile();
this._subs.push(this._bootstrapService.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this._bootstrapService.themeService.getColorTheme());
let profile: IConnectionProfile = this._bootstrapService.getOriginalConnectionProfile();
this.actionbar = new ActionBar(this.actionbarContainer.nativeElement);
this.actionbar.push(new RefreshWidgetAction(this.refresh, this), {
icon: true,
label: false,
});
this.actionbar.push(new EditDashboardAction(this.edit, this), {
icon: true,
label: false,
});
if (profile && (!profile.databaseName || Utils.isMaster(profile))) {
// Route to the server page as this is the default database
self._router.navigate(['server-dashboard']);
this._router.navigate(['server-dashboard']);
}
}
@@ -48,11 +65,23 @@ export class DashboardComponent implements OnInit, OnDestroy {
}
private updateTheme(theme: IColorTheme): void {
let headerEl = <HTMLElement> this.header.nativeElement;
let headerEl = <HTMLElement>this.header.nativeElement;
headerEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
headerEl.style.borderBottomWidth = '1px';
headerEl.style.borderBottomStyle = 'solid';
}
onActivate(page: DashboardPage) {
this._currentPage = page;
}
refresh(): void {
if (this._currentPage) {
this._currentPage.refresh();
}
}
edit(): void {
this._currentPage.enableEdit();
}
}

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.
*--------------------------------------------------------------------------------------------*/
.dashboardEditor .header .monaco-action-bar .action-label {
padding: 8px;
}
.dashboardEditor .header .monaco-action-bar .action-item {
margin-right: 5px;
}

View File

@@ -11,7 +11,6 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { DashboardInput } from './dashboardInput';
import { DashboardModule } from './dashboard.module';
@@ -66,7 +65,7 @@ export class DashboardEditor extends BaseEditor {
super.setInput(input, options);
$(parentElement).empty();
$(parentElement).clearChildren();
if (!input.hasBootstrapped) {
let container = DOM.$<HTMLElement>('.dashboardEditor');
@@ -105,8 +104,3 @@ export class DashboardEditor extends BaseEditor {
super.dispose();
}
}
ModesRegistry.registerLanguage({
extensions: ['.dashboard'],
id: 'dashboard',
});

View File

@@ -76,7 +76,7 @@ export class DashboardInput extends EditorInput {
public getResource(): URI {
return URI.from({
scheme: 'dashboard',
path: '.dashboard'
path: 'dashboard'
});
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnInit, Inject, forwardRef, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
@@ -12,10 +12,9 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDestroy {
export class DatabaseDashboardPage extends DashboardPage implements OnInit {
protected propertiesWidget: WidgetConfig = {
name: nls.localize('databasePageName', 'DATABASE DASHBOARD'),
widget: {
@@ -32,15 +31,15 @@ export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDe
};
protected readonly context = 'database';
private _dispose: IDisposable[] = [];
constructor(
@Inject(forwardRef(() => IBreadcrumbService)) private _breadcrumbService: IBreadcrumbService,
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(dashboardService);
this._dispose.push(dashboardService.onUpdatePage(() => {
super(dashboardService, el, _cd);
this._register(dashboardService.onUpdatePage(() => {
this.refresh(true);
this._cd.detectChanges();
}));
@@ -49,11 +48,5 @@ export class DatabaseDashboardPage extends DashboardPage implements OnInit, OnDe
ngOnInit() {
this._breadcrumbService.setBreadcrumbs(BreadcrumbClass.DatabasePage);
this.baseInit();
}
ngOnDestroy() {
this._dispose = dispose(this._dispose);
this.baseDestroy();
}
}

View File

@@ -120,6 +120,12 @@ export const databaseDashboardSettingSchema: IJSONSchema = {
},
sizey: {
type: 'number'
},
col: {
type: 'number'
},
row: {
type: 'number'
}
}
},
@@ -155,4 +161,4 @@ export const databaseDashboardSettingSchema: IJSONSchema = {
};
export const DATABASE_DASHBOARD_SETTING = 'dashboard.database.widgets';
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnInit, Inject, forwardRef, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { OnInit, Inject, forwardRef, ChangeDetectorRef, ElementRef } from '@angular/core';
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
@@ -14,7 +14,7 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as nls from 'vs/nls';
export class ServerDashboardPage extends DashboardPage implements OnInit, OnDestroy {
export class ServerDashboardPage extends DashboardPage implements OnInit {
protected propertiesWidget: WidgetConfig = {
name: nls.localize('serverPageName', 'SERVER DASHBOARD'),
widget: {
@@ -35,9 +35,10 @@ export class ServerDashboardPage extends DashboardPage implements OnInit, OnDest
constructor(
@Inject(forwardRef(() => IBreadcrumbService)) private breadcrumbService: IBreadcrumbService,
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(dashboardService);
super(dashboardService, el, cd);
// revert back to default database
this.dashboardService.connectionManagementService.changeDatabase('master').then(() => {
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = undefined;
@@ -49,10 +50,5 @@ export class ServerDashboardPage extends DashboardPage implements OnInit, OnDest
ngOnInit() {
this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
this.baseInit();
}
ngOnDestroy() {
this.baseDestroy();
}
}

View File

@@ -17,12 +17,10 @@ import { ConnectionManagementInfo } from 'sql/parts/connection/common/connection
import { IAdminService } from 'sql/parts/admin/common/adminService';
import { IQueryManagementService } from 'sql/parts/query/common/queryManagement';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces';
import { IPropertiesConfig } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { AngularEventType } from 'sql/services/angularEventing/angularEventingService';
import { AngularEventType, IAngularEvent } from 'sql/services/angularEventing/angularEventingService';
import { ProviderMetadata, DatabaseInfo, SimpleExecuteResult } from 'data';
@@ -31,7 +29,8 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ConfigurationEditingService, IConfigurationValue } from 'vs/workbench/services/configuration/node/configurationEditingService'
import { IMessageService } from 'vs/platform/message/common/message';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IStorageService } from 'vs/platform/storage/common/storage';
@@ -127,10 +126,14 @@ export class DashboardServiceInterface implements OnDestroy {
private _workspaceContextService: IWorkspaceContextService;
private _storageService: IStorageService;
private _capabilitiesService: ICapabilitiesService;
private _configurationEditingService: ConfigurationEditingService;
private _updatePage = new Emitter<void>();
public readonly onUpdatePage: Event<void> = this._updatePage.event;
private _onDeleteWidget = new Emitter<string>();
public readonly onDeleteWidget: Event<string> = this._onDeleteWidget.event;
constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
@Inject(forwardRef(() => Router)) private _router: Router,
@@ -145,6 +148,7 @@ export class DashboardServiceInterface implements OnDestroy {
this._workspaceContextService = this._bootstrapService.workspaceContextService;
this._storageService = this._bootstrapService.storageService;
this._capabilitiesService = this._bootstrapService.capabilitiesService;
this._configurationEditingService = this._bootstrapService.configurationEditorService;
}
ngOnDestroy() {
@@ -155,6 +159,10 @@ export class DashboardServiceInterface implements OnDestroy {
return this._messageService;
}
public get configurationEditingService(): ConfigurationEditingService {
return this._configurationEditingService;
}
public get metadataService(): SingleConnectionMetadataService {
return this._metadataService;
}
@@ -195,7 +203,7 @@ export class DashboardServiceInterface implements OnDestroy {
return this._storageService;
}
public get CapabilitiesService(): ICapabilitiesService {
public get capabilitiesService(): ICapabilitiesService {
return this._capabilitiesService;
}
@@ -241,13 +249,17 @@ export class DashboardServiceInterface implements OnDestroy {
* Get settings for given string
* @param type string of setting to get from dashboard settings; i.e dashboard.{type}
*/
public getSettings(type: string): { widgets: Array<WidgetConfig>, properties: boolean | IPropertiesConfig[] } {
let config = this._configService.getConfiguration(DASHBOARD_SETTINGS);
return config[type];
public getSettings<T>(type: string): T {
let config = this._configService.getValue<T>([DASHBOARD_SETTINGS, type].join('.'));
return config;
}
private handleDashboardEvent(event: AngularEventType): void {
switch (event) {
public writeSettings(key: string, value: any, target: ConfigurationTarget) {
this._configurationEditingService.writeConfiguration(target, { key: DASHBOARD_SETTINGS + '.' + key + '.widgets', value });
}
private handleDashboardEvent(event: IAngularEvent): void {
switch (event.event) {
case AngularEventType.NAV_DATABASE:
this.connectionManagementService.changeDatabase(this.connectionManagementService.connectionInfo.connectionProfile.databaseName).then(
result => {
@@ -269,6 +281,8 @@ export class DashboardServiceInterface implements OnDestroy {
case AngularEventType.NAV_SERVER:
this._router.navigate(['server-dashboard']);
break;
case AngularEventType.DELETE_WIDGET:
this._onDeleteWidget.fire(event.payload.id);
}
}
}

View File

@@ -1,89 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { IConnectionManagementService, MetadataType } from 'sql/parts/connection/common/connectionManagement';
import {
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction,
BackupAction, BaseActionContext, ManageAction
} from 'sql/workbench/common/actions';
import { IDisasterRecoveryUiService } from 'sql/parts/disasterRecovery/common/interfaces';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction } from 'vs/base/common/actions';
export function GetExplorerActions(type: MetadataType, isCloud: boolean, dashboardService: DashboardServiceInterface): TPromise<IAction[]> {
let actions: IAction[] = [];
// When context menu on database
if (type === undefined) {
actions.push(dashboardService.instantiationService.createInstance(DashboardNewQueryAction, DashboardNewQueryAction.ID, NewQueryAction.LABEL, NewQueryAction.ICON));
if (!isCloud) {
actions.push(dashboardService.instantiationService.createInstance(DashboardBackupAction, DashboardBackupAction.ID, DashboardBackupAction.LABEL));
}
actions.push(dashboardService.instantiationService.createInstance(ManageAction, ManageAction.ID, ManageAction.LABEL));
return TPromise.as(actions);
}
if (type === MetadataType.View || type === MetadataType.Table) {
actions.push(dashboardService.instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, ScriptSelectAction.LABEL));
}
if (type === MetadataType.Table) {
actions.push(dashboardService.instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL));
}
actions.push(dashboardService.instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
return TPromise.as(actions);
}
export class DashboardBackupAction extends BackupAction {
public static ID = 'dashboard.' + BackupAction.ID;
constructor(
id: string, label: string,
@IDisasterRecoveryUiService disasterRecoveryService: IDisasterRecoveryUiService,
@IConnectionManagementService private connectionManagementService: IConnectionManagementService
) {
super(id, label, BackupAction.ICON, disasterRecoveryService, );
}
run(actionContext: BaseActionContext): TPromise<boolean> {
let self = this;
// change database before performing action
return new TPromise<boolean>((resolve, reject) => {
self.connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
actionContext.connInfo = self.connectionManagementService.getConnectionInfo(actionContext.uri);
super.run(actionContext).then((result) => {
resolve(result);
});
},
() => {
resolve(false);
});
});
}
}
export class DashboardNewQueryAction extends NewQueryAction {
public static ID = 'dashboard.' + NewQueryAction.ID;
run(actionContext: BaseActionContext): TPromise<boolean> {
let self = this;
// change database before performing action
return new TPromise<boolean>((resolve, reject) => {
self._connectionManagementService.changeDatabase(actionContext.uri, actionContext.profile.databaseName).then(() => {
actionContext.profile = self._connectionManagementService.getConnectionProfile(actionContext.uri);
super.run(actionContext).then((result) => {
resolve(result);
});
},
() => {
resolve(false);
});
});
}
}

View File

@@ -0,0 +1,392 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Router } from '@angular/router';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { MetadataType } from 'sql/parts/connection/common/connectionManagement';
import { SingleConnectionManagementService } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import {
NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction, ScriptExecuteAction, ScriptAlterAction,
BackupAction, ManageActionContext, BaseActionContext, ManageAction, RestoreAction
} from 'sql/workbench/common/actions';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import * as Constants from 'sql/parts/connection/common/constants';
import { ObjectMetadata } from 'data';
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IAction } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { generateUuid } from 'vs/base/common/uuid';
import { $ } from 'vs/base/browser/dom';
export class ObjectMetadataWrapper implements ObjectMetadata {
public metadataType: MetadataType;
public metadataTypeName: string;
public urn: string;
public name: string;
public schema: string;
constructor(from?: ObjectMetadata) {
if (from) {
this.metadataType = from.metadataType;
this.metadataTypeName = from.metadataTypeName;
this.urn = from.urn;
this.name = from.name;
this.schema = from.schema;
}
}
public matches(other: ObjectMetadataWrapper): boolean {
if (!other) {
return false;
}
return this.metadataType === other.metadataType
&& this.schema === other.schema
&& this.name === other.name;
}
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
if (!objectMetadata) {
return undefined;
}
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
}
// custom sort : Table > View > Stored Procedures > Function
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
// compare the object type
if (metadata1.metadataType < metadata2.metadataType) {
return -1;
} else if (metadata1.metadataType > metadata2.metadataType) {
return 1;
// otherwise compare the schema
} else {
let schemaCompare: number = metadata1.schema && metadata2.schema
? metadata1.schema.localeCompare(metadata2.schema)
// schemas are not expected to be undefined, but if they are then compare using object names
: 0;
if (schemaCompare !== 0) {
return schemaCompare;
// otherwise compare the object name
} else {
return metadata1.name.localeCompare(metadata2.name);
}
}
}
}
export declare type TreeResource = IConnectionProfile | ObjectMetadataWrapper;
// Empty class just for tree input
export class ExplorerModel {
public static readonly id = generateUuid();
}
export class ExplorerController extends TreeDefaults.DefaultController {
constructor(
// URI for the dashboard for managing, should look into some other way of doing this
private _uri,
private _connectionService: SingleConnectionManagementService,
private _router: Router,
private _contextMenuService: IContextMenuService,
private _capabilitiesService: ICapabilitiesService,
private _instantiationService: IInstantiationService
) {
super();
}
protected onLeftClick(tree: tree.ITree, element: TreeResource, event: IMouseEvent, origin: string = 'mouse'): boolean {
const payload = { origin: origin };
const isDoubleClick = (origin === 'mouse' && event.detail === 2);
// Cancel Event
const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown';
if (!isMouseDown) {
event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
}
event.stopPropagation();
tree.setFocus(element, payload);
if (!(element instanceof ObjectMetadataWrapper) && isDoubleClick) {
event.preventDefault(); // focus moves to editor, we need to prevent default
this.handleItemDoubleClick(element);
} else {
tree.setFocus(element, payload);
tree.setSelection([element], payload);
}
return true;
}
public onContextMenu(tree: tree.ITree, element: TreeResource, event: tree.ContextMenuEvent): boolean {
let context: ManageActionContext | BaseActionContext;
if (element instanceof ObjectMetadataWrapper) {
context = {
object: element,
profile: this._connectionService.connectionInfo.connectionProfile
};
} else {
context = {
profile: element,
uri: this._uri
};
}
this._contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => GetExplorerActions(element, this._instantiationService, this._capabilitiesService, this._connectionService.connectionInfo),
getActionsContext: () => context
});
return true;
}
private handleItemDoubleClick(element: IConnectionProfile): void {
this._connectionService.changeDatabase(element.databaseName).then(result => {
this._router.navigate(['database-dashboard']);
});
}
}
export class ExplorerDataSource implements tree.IDataSource {
private _data: TreeResource[];
public getId(tree: tree.ITree, element: TreeResource | ExplorerModel): string {
if (element instanceof ObjectMetadataWrapper) {
return element.urn || element.schema + element.name;
} else if (element instanceof ExplorerModel) {
return ExplorerModel.id;
} else {
return (element as IConnectionProfile).getOptionsKey();
}
}
public hasChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): boolean {
if (element instanceof ExplorerModel) {
return true;
} else {
return false;
}
}
public getChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise {
if (element instanceof ExplorerModel) {
return TPromise.as(this._data);
} else {
return TPromise.as(undefined);
}
}
public getParent(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise {
if (element instanceof ExplorerModel) {
return TPromise.as(undefined);
} else {
return TPromise.as(new ExplorerModel());
}
}
public set data(data: TreeResource[]) {
this._data = data;
}
}
enum TEMPLATEIDS {
profile = 'profile',
object = 'object'
}
export interface IListTemplate {
icon?: HTMLElement;
label: HTMLElement;
}
export class ExplorerRenderer implements tree.IRenderer {
public getHeight(tree: tree.ITree, element: TreeResource): number {
return 22;
}
public getTemplateId(tree: tree.ITree, element: TreeResource): string {
if (element instanceof ObjectMetadataWrapper) {
return TEMPLATEIDS.object;
} else {
return TEMPLATEIDS.profile;
}
}
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate {
let row = $('.list-row');
let label = $('.label');
let icon: HTMLElement;
if (templateId === TEMPLATEIDS.object) {
icon = $('div');
} else {
icon = $('.icon.database');
}
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
public renderElement(tree: tree.ITree, element: TreeResource, templateId: string, templateData: IListTemplate): void {
if (element instanceof ObjectMetadataWrapper) {
switch (element.metadataType) {
case MetadataType.Function:
templateData.icon.className = 'icon scalarvaluedfunction';
break;
case MetadataType.SProc:
templateData.icon.className = 'icon stored-procedure';
break;
case MetadataType.Table:
templateData.icon.className = 'icon table';
break;
case MetadataType.View:
templateData.icon.className = 'icon view';
break;
}
templateData.label.innerText = element.schema + '.' + element.name;
} else {
templateData.label.innerText = element.databaseName;
}
}
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void {
// no op
}
}
export class ExplorerFilter implements tree.IFilter {
private _filterString: string;
public isVisible(tree: tree.ITree, element: TreeResource): boolean {
if (element instanceof ObjectMetadataWrapper) {
return this._doIsVisibleObjectMetadata(element);
} else {
return this._doIsVisibleConnectionProfile(element);
}
}
// apply filter to databasename of the profile
private _doIsVisibleConnectionProfile(element: IConnectionProfile): boolean {
if (!this._filterString) {
return true;
}
let filterString = this._filterString.trim().toLowerCase();
return element.databaseName.toLowerCase().includes(filterString);
}
// apply filter for objectmetadatawrapper
// could be improved by pre-processing the filter string
private _doIsVisibleObjectMetadata(element: ObjectMetadataWrapper): boolean {
if (!this._filterString) {
return true;
}
// freeze filter string for edge cases
let filterString = this._filterString.trim().toLowerCase();
// determine if a filter is applied
let metadataType: MetadataType;
if (filterString.includes(':')) {
let filterArray = filterString.split(':');
if (filterArray.length > 2) {
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
} else {
filterString = filterArray[1];
}
switch (filterArray[0].toLowerCase()) {
case 'v':
metadataType = MetadataType.View;
break;
case 't':
metadataType = MetadataType.Table;
break;
case 'sp':
metadataType = MetadataType.SProc;
break;
case 'f':
metadataType = MetadataType.Function;
break;
case 'a':
return true;
default:
break;
}
}
if (metadataType !== undefined) {
return element.metadataType === metadataType && (element.schema + '.' + element.name).toLowerCase().includes(filterString);
} else {
return (element.schema + '.' + element.name).toLowerCase().includes(filterString);
}
}
public set filterString(val: string) {
this._filterString = val;
}
}
function GetExplorerActions(element: TreeResource, instantiationService: IInstantiationService, capabilitiesService: ICapabilitiesService, info: ConnectionManagementInfo): TPromise<IAction[]> {
let actions: IAction[] = [];
if (element instanceof ObjectMetadataWrapper) {
if (element.metadataType === MetadataType.View || element.metadataType === MetadataType.Table) {
actions.push(instantiationService.createInstance(ScriptSelectAction, ScriptSelectAction.ID, ScriptSelectAction.LABEL));
}
if (element.metadataType === MetadataType.Table) {
actions.push(instantiationService.createInstance(EditDataAction, EditDataAction.ID, EditDataAction.LABEL));
}
if (element.metadataType === MetadataType.SProc && info.connectionProfile.providerName === Constants.mssqlProviderName) {
actions.push(instantiationService.createInstance(ScriptExecuteAction, ScriptExecuteAction.ID, ScriptExecuteAction.LABEL));
}
if ((element.metadataType === MetadataType.SProc || element.metadataType === MetadataType.Function || element.metadataType === MetadataType.View)
&& info.connectionProfile.providerName === Constants.mssqlProviderName) {
actions.push(instantiationService.createInstance(ScriptAlterAction, ScriptAlterAction.ID, ScriptAlterAction.LABEL));
}
} else {
actions.push(instantiationService.createInstance(NewQueryAction, NewQueryAction.ID, NewQueryAction.LABEL, NewQueryAction.ICON));
let action: IAction = instantiationService.createInstance(RestoreAction, RestoreAction.ID, RestoreAction.LABEL, RestoreAction.ICON);
if (capabilitiesService.isFeatureAvailable(action, info)) {
actions.push(action);
}
action = instantiationService.createInstance(BackupAction, BackupAction.ID, BackupAction.LABEL, BackupAction.ICON);
if (capabilitiesService.isFeatureAvailable(action, info)) {
actions.push(action);
}
actions.push(instantiationService.createInstance(ManageAction, ManageAction.ID, ManageAction.LABEL));
return TPromise.as(actions);
}
actions.push(instantiationService.createInstance(ScriptCreateAction, ScriptCreateAction.ID, ScriptCreateAction.LABEL));
return TPromise.as(actions);
}

View File

@@ -9,4 +9,4 @@
<div style="flex: 1 1 auto; position: relative">
<div #table style="position: absolute; height: 100%; width: 100%"></div>
</div>
</div>
</div>

View File

@@ -7,196 +7,41 @@ import 'vs/css!sql/media/objectTypes/objecttypes';
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!./media/explorerWidget';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Router } from '@angular/router';
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { MetadataType } from 'sql/parts/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { BaseActionContext } from 'sql/workbench/common/actions';
import { GetExplorerActions } from './explorerActions';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { warn } from 'sql/base/common/log';
import { MultipleRequestDelayer } from 'sql/base/common/async';
import { ExplorerFilter, ExplorerRenderer, ExplorerDataSource, ExplorerController, ObjectMetadataWrapper, ExplorerModel } from './explorerTree';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { IDisposable } from 'vs/base/common/lifecycle';
import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler';
import * as nls from 'vs/nls';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import * as types from 'vs/base/common/types';
import { $, getContentHeight } from 'vs/base/browser/dom';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { getContentHeight } from 'vs/base/browser/dom';
import { Delayer } from 'vs/base/common/async';
import { ObjectMetadata } from 'data';
export class ObjectMetadataWrapper implements ObjectMetadata {
public metadataType: MetadataType;
public metadataTypeName: string;
public urn: string;
public name: string;
public schema: string;
constructor(from?: ObjectMetadata) {
if (from) {
this.metadataType = from.metadataType;
this.metadataTypeName = from.metadataTypeName;
this.urn = from.urn;
this.name = from.name;
this.schema = from.schema;
}
}
public matches(other: ObjectMetadataWrapper): boolean {
if (!other) {
return false;
}
return this.metadataType === other.metadataType
&& this.schema === other.schema
&& this.name === other.name;
}
public static createFromObjectMetadata(objectMetadata: ObjectMetadata[]): ObjectMetadataWrapper[] {
if (!objectMetadata) {
return undefined;
}
return objectMetadata.map(m => new ObjectMetadataWrapper(m));
}
// custom sort : Table > View > Stored Procedures > Function
public static sort(metadata1: ObjectMetadataWrapper, metadata2: ObjectMetadataWrapper): number {
// compare the object type
if (metadata1.metadataType < metadata2.metadataType) {
return -1;
} else if (metadata1.metadataType > metadata2.metadataType) {
return 1;
// otherwise compare the schema
} else {
let schemaCompare: number = metadata1.schema && metadata2.schema
? metadata1.schema.localeCompare(metadata2.schema)
// schemas are not expected to be undefined, but if they are then compare using object names
: 0;
if (schemaCompare !== 0) {
return schemaCompare;
// otherwise compare the object name
} else {
return metadata1.name.localeCompare(metadata2.name);
}
}
}
}
declare type ListResource = string | ObjectMetadataWrapper;
enum TemplateIds {
STRING = 'string',
METADATA = 'metadata'
}
interface IListTemplate {
icon?: HTMLElement;
label: HTMLElement;
}
class Delegate implements IDelegate<ListResource> {
getHeight(element: ListResource): number {
return 22;
}
getTemplateId(element: ListResource): string {
if (element instanceof ObjectMetadataWrapper) {
return TemplateIds.METADATA.toString();
} else if (types.isString(element)) {
return TemplateIds.STRING.toString();
} else {
return '';
}
}
}
class StringRenderer implements IRenderer<string, IListTemplate> {
public readonly templateId = TemplateIds.STRING.toString();
renderTemplate(container: HTMLElement): IListTemplate {
let row = $('.list-row');
let icon = $('.icon.database');
let label = $('.label');
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
renderElement(element: string, index: number, templateData: IListTemplate): void {
templateData.label.innerText = element;
}
disposeTemplate(templateData: IListTemplate): void {
// no op
}
}
class MetadataRenderer implements IRenderer<ObjectMetadataWrapper, IListTemplate> {
public readonly templateId = TemplateIds.METADATA.toString();
renderTemplate(container: HTMLElement): IListTemplate {
let row = $('.list-row');
let icon = $('div');
let label = $('.label');
row.appendChild(icon);
row.appendChild(label);
container.appendChild(row);
return { icon, label };
}
renderElement(element: ObjectMetadataWrapper, index: number, templateData: IListTemplate): void {
if (element && element) {
switch (element.metadataType) {
case MetadataType.Function:
templateData.icon.className = 'icon scalarvaluedfunction';
break;
case MetadataType.SProc:
templateData.icon.className = 'icon stored-procedure';
break;
case MetadataType.Table:
templateData.icon.className = 'icon table';
break;
case MetadataType.View:
templateData.icon.className = 'icon view';
break;
}
templateData.label.innerText = element.schema + '.' + element.name;
}
}
disposeTemplate(templateData: IListTemplate): void {
// no op
}
}
@Component({
selector: 'explorer-widget',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/explorer/explorerWidget.component.html'))
})
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
private _isCloud: boolean;
private _tableData: ListResource[];
private _disposables: Array<IDisposable> = [];
export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit {
private _input: InputBox;
private _table: List<ListResource>;
private _lastClickedItem: ListResource;
private _tree: Tree;
private _filterDelayer = new Delayer<void>(200);
private _dblClickDelayer = new MultipleRequestDelayer<void>(500);
private _treeController = new ExplorerController(
this._bootstrap.getUnderlyingUri(),
this._bootstrap.connectionManagementService,
this._router,
this._bootstrap.contextMenuService,
this._bootstrap.capabilitiesService,
this._bootstrap.instantiationService
);
private _treeRenderer = new ExplorerRenderer();
private _treeDataSource = new ExplorerDataSource();
private _treeFilter = new ExplorerFilter();
@ViewChild('input') private _inputContainer: ElementRef;
@ViewChild('table') private _tableContainer: ElementRef;
@@ -209,7 +54,6 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
) {
super();
this._isCloud = _bootstrap.connectionManagementService.connectionInfo.serverInfo.isCloud;
this.init();
}
@@ -218,41 +62,34 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
placeholder: this._config.context === 'database' ? nls.localize('seachObjects', 'Search by name of type (a:, t:, v:, f:, or sp:)') : nls.localize('searchDatabases', 'Search databases')
};
this._input = new InputBox(this._inputContainer.nativeElement, this._bootstrap.contextViewService, inputOptions);
this._disposables.push(this._input.onDidChange(e => {
this._register(this._input.onDidChange(e => {
this._filterDelayer.trigger(() => {
this._table.splice(0, this._table.length, this._filterTable(e));
this._treeFilter.filterString = e;
this._tree.refresh();
});
}));
this._table = new List<ListResource>(this._tableContainer.nativeElement, new Delegate(), [new MetadataRenderer(), new StringRenderer()]);
this._disposables.push(this._table.onContextMenu(e => {
this.handleContextMenu(e.element, e.index, e.anchor);
}));
this._disposables.push(this._table.onSelectionChange(e => {
if (e.elements.length > 0 && this._lastClickedItem === e.elements[0]) {
this._dblClickDelayer.trigger(() => this.handleItemDoubleClick(e.elements[0]));
} else {
this._lastClickedItem = e.elements.length > 0 ? e.elements[0] : undefined;
}
}));
this._table.layout(getContentHeight(this._tableContainer.nativeElement));
this._disposables.push(this._input);
this._disposables.push(attachInputBoxStyler(this._input, this._bootstrap.themeService));
this._disposables.push(this._table);
this._disposables.push(attachListStyler(this._table, this._bootstrap.themeService));
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
});
this._tree.layout(getContentHeight(this._tableContainer.nativeElement));
this._register(this._input);
this._register(attachInputBoxStyler(this._input, this._bootstrap.themeService));
this._register(this._tree);
this._register(attachListStyler(this._tree, this._bootstrap.themeService));
}
private init(): void {
if (this._config.context === 'database') {
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
this._register(toDisposableSubscription(this._bootstrap.metadataService.metadata.subscribe(
data => {
if (data) {
this._tableData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
this._tableData.sort(ObjectMetadataWrapper.sort);
this._table.splice(0, this._table.length, this._tableData);
let objectData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata);
objectData.sort(ObjectMetadataWrapper.sort);
this._treeDataSource.data = objectData;
this._tree.setInput(new ExplorerModel());
}
},
error => {
@@ -260,10 +97,16 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
}
)));
} else {
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
let currentProfile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
this._register(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
data => {
this._tableData = data;
this._table.splice(0, this._table.length, this._tableData);
let profileData = data.map(d => {
let profile = new ConnectionProfile(currentProfile.serverCapabilities, currentProfile);
profile.databaseName = d;
return profile;
});
this._treeDataSource.data = profileData;
this._tree.setInput(new ExplorerModel());
},
error => {
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.databaseError', "Unable to load databases");
@@ -272,123 +115,7 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget,
}
}
/**
* Handles action when an item is double clicked in the explorer widget
* @param val If on server page, explorer objects will be strings representing databases;
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
*
*/
private handleItemDoubleClick(val: ListResource): void {
if (types.isString(val)) {
this._bootstrap.connectionManagementService.changeDatabase(val as string).then(result => {
this._router.navigate(['database-dashboard']);
});
}
}
/**
* Handles action when a item is clicked in the explorer widget
* @param val If on server page, explorer objects will be strings representing databases;
* If on databasepage, explorer objects will be ObjectMetadataWrapper representing object types;
* @param index Index of the value in the array the ngFor template is built from
* @param event Click event
*/
private handleContextMenu(val: ListResource, index: number, anchor: HTMLElement | { x: number, y: number }): void {
// event will exist if the context menu span was clicked
if (event) {
if (this._config.context === 'server') {
let newProfile = <IConnectionProfile>Object.create(this._bootstrap.connectionManagementService.connectionInfo.connectionProfile);
newProfile.databaseName = val as string;
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => GetExplorerActions(undefined, this._isCloud, this._bootstrap),
getActionsContext: () => {
return <BaseActionContext>{
uri: this._bootstrap.getUnderlyingUri(),
profile: newProfile,
connInfo: this._bootstrap.connectionManagementService.connectionInfo,
databasename: val as string
};
}
});
} else if (this._config.context === 'database') {
let object = val as ObjectMetadataWrapper;
this._bootstrap.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => GetExplorerActions(object.metadataType, this._isCloud, this._bootstrap),
getActionsContext: () => {
return <BaseActionContext>{
object: object,
uri: this._bootstrap.getUnderlyingUri(),
profile: this._bootstrap.connectionManagementService.connectionInfo.connectionProfile
};
}
});
} else {
warn('Unknown dashboard context: ', this._config.context);
}
}
this._changeRef.detectChanges();
}
private _filterTable(val: string): ListResource[] {
let items = this._tableData;
if (!items) {
return items;
}
// format filter string for clean filter, no white space and lower case
let filterString = val.trim().toLowerCase();
// handle case when passed a string array
if (types.isString(items[0])) {
let _items = <string[]>items;
return _items.filter(item => {
return item.toLowerCase().includes(filterString);
});
}
// make typescript compiler happy
let objectItems = items as ObjectMetadataWrapper[];
// determine is a filter is applied
let metadataType: MetadataType;
if (val.includes(':')) {
let filterArray = filterString.split(':');
if (filterArray.length > 2) {
filterString = filterArray.slice(1, filterArray.length - 1).join(':');
} else {
filterString = filterArray[1];
}
switch (filterArray[0].toLowerCase()) {
case 'v':
metadataType = MetadataType.View;
break;
case 't':
metadataType = MetadataType.Table;
break;
case 'sp':
metadataType = MetadataType.SProc;
break;
case 'f':
metadataType = MetadataType.Function;
break;
case 'a':
return objectItems;
default:
break;
}
}
return objectItems.filter(item => {
if (metadataType !== undefined) {
return item.metadataType === metadataType && (item.schema + '.' + item.name).toLowerCase().includes(filterString);
} else {
return (item.schema + '.' + item.name).toLowerCase().includes(filterString);
}
});
public refresh(): void {
this.init();
}
}

View File

@@ -11,4 +11,5 @@ explorer-widget .list-row {
display: flex;
flex-direction: row;
align-items: center;
}
margin-left: -33px;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import {
Component, Inject, ViewContainerRef, forwardRef, AfterContentInit,
ComponentFactoryResolver, ViewChild, OnDestroy, ChangeDetectorRef
ComponentFactoryResolver, ViewChild, ChangeDetectorRef
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
@@ -15,39 +15,45 @@ import { InsightAction, InsightActionContext } from 'sql/workbench/common/action
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { IInsightsConfig, IInsightsView } from './interfaces';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { insertValueRegex } from 'sql/parts/insights/browser/insightsDialogView';
import { insertValueRegex } from 'sql/parts/insights/common/interfaces';
import { RunInsightQueryAction } from './actions';
import { SimpleExecuteResult } from 'data';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Action } from 'vs/base/common/actions';
import * as types from 'vs/base/common/types';
import * as pfs from 'vs/base/node/pfs';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { WorkbenchState } from 'vs/platform/workspace/common/workspace';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
interface IStorageResult {
date: string;
results: SimpleExecuteResult;
}
@Component({
selector: 'insights-widget',
template: `
<div *ngIf="error" style="text-align: center; padding-top: 20px">{{error}}</div>
<div *ngIf="lastUpdated" style="font-style: italic; font-size: 80%; margin-left: 5px">{{lastUpdated}}</div>
<div style="margin: 10px; width: calc(100% - 20px); height: calc(100% - 20px)">
<ng-template component-host></ng-template>
</div>`,
styles: [':host { width: 100%; height: 100%}']
styles: [':host { width: 100%; height: 100% }']
})
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit, OnDestroy {
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit {
private insightConfig: IInsightsConfig;
private queryObv: Observable<SimpleExecuteResult>;
private _disposables: Array<IDisposable> = [];
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
private _typeKey: string;
private _init: boolean = false;
public error: string;
public lastUpdated: string;
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
@@ -90,7 +96,7 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
ngAfterContentInit() {
this._init = true;
if (this.queryObv) {
this._disposables.push(toDisposableSubscription(this.queryObv.subscribe(
this._register(toDisposableSubscription(this.queryObv.subscribe(
result => {
this._updateChild(result);
},
@@ -101,10 +107,6 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
}
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
}
private showError(error: string): void {
this.error = error;
this._cd.detectChanges();
@@ -128,7 +130,11 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
private _storeResult(result: SimpleExecuteResult): SimpleExecuteResult {
if (this.insightConfig.cacheId) {
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(result));
let store: IStorageResult = {
date: new Date().toString(),
results: result
};
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(store));
}
return result;
}
@@ -137,8 +143,12 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
if (this.insightConfig.cacheId) {
let storage = this.dashboardService.storageService.get(this._getStorageKey());
if (storage) {
let storedResult: IStorageResult = JSON.parse(storage);
let date = new Date(storedResult.date);
this.lastUpdated = nls.localize('insights.lastUpdated', "Last Updated: {0} {1}", date.toLocaleTimeString(), date.toLocaleDateString());
if (this._init) {
this._updateChild(JSON.parse(storage));
this._updateChild(storedResult.results);
this._cd.detectChanges();
} else {
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(JSON.parse(storage)));
}
@@ -151,17 +161,11 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
return false;
}
public get refresh(): () => void {
return this._refresh();
}
public _refresh(): () => void {
return () => {
this._runQuery().then(
result => this._updateChild(result),
error => this.showError(error)
);
};
public refresh(): void {
this._runQuery().then(
result => this._updateChild(result),
error => this.showError(error)
);
}
private _getStorageKey(): string {
@@ -192,7 +196,10 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
let componentInstance = componentRef.instance;
componentInstance.data = { columns: result.columnInfo.map(item => item.columnName), rows: result.rows.map(row => row.map(item => item.displayValue)) };
// check if the setter is defined
componentInstance.config = this.insightConfig.type[this._typeKey];
if (componentInstance.setConfig) {
componentInstance.setConfig(this.insightConfig.type[this._typeKey]);
}
if (componentInstance.init) {
componentInstance.init();
}
@@ -239,7 +246,28 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
let match = filePath.match(insertValueRegex);
if (match && match.length > 0 && match[1] === 'workspaceRoot') {
filePath = filePath.replace(match[0], '');
filePath = this.dashboardService.workspaceContextService.toResource(filePath).fsPath;
//filePath = this.dashboardService.workspaceContextService.toResource(filePath).fsPath;
switch (this.dashboardService.workspaceContextService.getWorkbenchState()) {
case WorkbenchState.FOLDER:
filePath = this.dashboardService.workspaceContextService.getWorkspace().folders[0].toResource(filePath).fsPath;
break;
case WorkbenchState.WORKSPACE:
let filePathArray = filePath.split('/');
// filter out empty sections
filePathArray = filePathArray.filter(i => !!i);
let folder = this.dashboardService.workspaceContextService.getWorkspace().folders.find(i => i.name === filePathArray[0]);
if (!folder) {
return Promise.reject<void[]>(new Error(`Could not find workspace folder ${filePathArray[0]}`));
}
// remove the folder name from the filepath
filePathArray.shift();
// rejoin the filepath after doing the work to find the right folder
filePath = '/' + filePathArray.join('/');
filePath = folder.toResource(filePath).fsPath;
break;
}
}
promises.push(new Promise((resolve, reject) => {
pfs.readFile(filePath).then(
@@ -256,4 +284,4 @@ export class InsightsWidget extends DashboardWidget implements IDashboardWidget,
return Promise.all(promises);
}
}
}

View File

@@ -4,19 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { IInsightRegistry, Extensions as InsightExtensions } from 'sql/platform/dashboard/common/insightRegistry';
import { ITaskRegistry, Extensions as TaskExtensions } from 'sql/platform/tasks/taskRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
import * as nls from 'vs/nls';
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
const taskRegistry = Registry.as<ITaskRegistry>(TaskExtensions.TaskContribution);
export const insightsSchema: IJSONSchema = {
type: 'object',
description: nls.localize('insightWidgetDescription', 'Adds a widget that can query a server or database and display the results in multiple ways - as a chart, summarized count, and more'),
properties: {
id: {
cacheId: {
type: 'string',
description: nls.localize('insightIdDescription', 'Unique Identifier used for cacheing the results of the insight.')
},
@@ -87,8 +85,11 @@ export const insightsSchema: IJSONSchema = {
type: 'object',
properties: {
types: {
type: 'object',
properties: taskRegistry.taskSchemas
description: nls.localize('actionTypes', "Which actions to use"),
type: 'array',
items: {
type: 'string'
}
},
database: {
type: 'string',

View File

@@ -38,7 +38,7 @@ export interface IInsightData {
export interface IInsightsView {
data: IInsightData;
config?: { [key: string]: any };
setConfig?: (config: { [key: string]: any }) => void;
init?: () => void;
}

View File

@@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, ViewChild } from '@angular/core';
import { BaseChartDirective } from 'ng2-charts/ng2-charts';
/* SQL Imports */
@@ -18,7 +18,7 @@ import * as colors from 'vs/platform/theme/common/colorRegistry';
import { mixin } from 'sql/base/common/objects';
import { Color } from 'vs/base/common/color';
import * as types from 'vs/base/common/types';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
export enum ChartType {
@@ -99,14 +99,13 @@ export const defaultChartConfig: IChartConfig = {
[options]="_options"></canvas>
</div>`
})
export abstract class ChartInsight implements IInsightsView, OnDestroy {
export abstract class ChartInsight extends Disposable implements IInsightsView {
private _isDataAvailable: boolean = false;
private _options: any = {};
@ViewChild(BaseChartDirective) private _chart: BaseChartDirective;
protected _defaultConfig = defaultChartConfig;
protected _disposables: Array<IDisposable> = [];
protected _config: IChartConfig;
protected _data: IInsightData;
@@ -116,14 +115,13 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(BOOTSTRAP_SERVICE_ID) protected _bootstrapService: IBootstrapService) { }
ngOnDestroy() {
this._disposables.forEach(item => item.dispose());
@Inject(BOOTSTRAP_SERVICE_ID) protected _bootstrapService: IBootstrapService
) {
super();
}
init() {
this._disposables.push(this._bootstrapService.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this._register(this._bootstrapService.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this.updateTheme(this._bootstrapService.themeService.getColorTheme());
// Note: must use a boolean to not render the canvas until all properties such as the labels and chart type are set.
// This is because chart.js doesn't auto-update anything other than dataset when re-rendering so defaults are used
@@ -150,10 +148,12 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
}
protected updateTheme(e: IColorTheme): void {
let foregroundColor = e.getColor(colors.editorForeground);
let foreground = foregroundColor ? foregroundColor.toString() : null;
let options = {
legend: {
labels: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
}
}
};
@@ -189,7 +189,7 @@ export abstract class ChartInsight implements IInsightsView, OnDestroy {
unmemoize(this, 'colors');
}
@Input() set config(config: IChartConfig) {
public setConfig(config: IChartConfig) {
this.clearMemoize();
this._config = mixin(config, this._defaultConfig, false);
this.legendPosition = this._config.legendPosition;

View File

@@ -3,40 +3,145 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChartInsight, ChartType, customMixin } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { ChartInsight, ChartType, customMixin, IChartConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { mixin } from 'sql/base/common/objects';
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
export interface IBarChartConfig extends IChartConfig {
yAxisMin: number;
yAxisMax: number;
yAxisLabel: string;
xAxisMin: number;
xAxisMax: number;
xAxisLabel: string;
}
export default class BarChart extends ChartInsight {
protected readonly chartType: ChartType = ChartType.Bar;
public setConfig(config: IBarChartConfig): void {
let options = {};
if (config.xAxisMax) {
let opts = {
scales: {
xAxes: [{
display: true,
ticks: {
max: config.xAxisMax
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.xAxisMin) {
let opts = {
scales: {
xAxes: [{
display: true,
ticks: {
min: config.xAxisMin
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.xAxisLabel) {
let opts = {
scales: {
xAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: config.xAxisLabel
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.yAxisMax) {
let opts = {
scales: {
yAxes: [{
display: true,
ticks: {
max: config.yAxisMax
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.yAxisMin) {
let opts = {
scales: {
yAxes: [{
display: true,
ticks: {
max: config.yAxisMin
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
if (config.yAxisLabel) {
let opts = {
scales: {
yAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: config.yAxisLabel
}
}]
}
};
options = mixin({}, mixin(options, opts, true, customMixin));
}
this.options = mixin({}, mixin(this.options, options, true, customMixin));
super.setConfig(config);
}
protected updateTheme(e: IColorTheme): void {
super.updateTheme(e);
let foregroundColor = e.getColor(colors.editorForeground);
let foreground = foregroundColor ? foregroundColor.toString() : null;
let gridLinesColor = e.getColor(editorLineNumbers);
let gridLines = gridLinesColor ? gridLinesColor.toString() : null;
let options = {
scales: {
xAxes: [{
scaleLabel: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
ticks: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
gridLines: {
color: e.getColor(editorLineNumbers)
color: gridLines
}
}],
yAxes: [{
scaleLabel: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
ticks: {
fontColor: e.getColor(colors.editorForeground)
fontColor: foreground
},
gridLines: {
color: e.getColor(editorLineNumbers)
color: gridLines
}
}]
}

View File

@@ -4,13 +4,40 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import BarChart from './barChart.component';
const properties: IJSONSchema = {
export const properties: IJSONSchema = {
properties: {
yAxisMin: {
type: 'number',
description: nls.localize('yAxisMin', "Minumum value of the y axis")
},
yAxisMax: {
type: 'number',
description: nls.localize('yAxisMax', "Maximum value of the y axis")
},
yAxisLabel: {
type: 'string',
description: nls.localize('yAxisLabel', "Label for the y axis")
},
xAxisMin: {
type: 'number',
description: nls.localize('xAxisMin', "Minumum value of the x axis")
},
xAxisMax: {
type: 'number',
description: nls.localize('xAxisMax', "Maximum value of the x axis")
},
xAxisLabel: {
type: 'string',
description: nls.localize('xAxisLabel', "Label for the x axis")
}
}
};
const barSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;

View File

@@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import HorizontalBarChart from './horizontalBarChart.component';
@@ -13,6 +14,6 @@ const properties: IJSONSchema = {
};
const horizontalBarSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
const horizontalBarSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('horizontalBar', '', horizontalBarSchema, HorizontalBarChart);

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChartType, customMixin, IChartConfig, defaultChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import BarChart from './barChart.component';
import { ChartType, customMixin, defaultChartConfig, IDataSet, IPointDataSet } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import BarChart, { IBarChartConfig } from './barChart.component';
import { memoize, unmemoize } from 'sql/base/common/decorators';
import { mixin } from 'sql/base/common/objects';
import { clone } from 'vs/base/common/objects';
@@ -14,7 +14,7 @@ export enum DataType {
Point = 'point'
}
export interface ILineConfig extends IChartConfig {
export interface ILineConfig extends IBarChartConfig {
dataType?: DataType;
}
@@ -69,8 +69,8 @@ export default class LineChart extends BarChart {
}
protected addAxisLabels(): void {
let xLabel = this._data.columns[1] || 'x';
let yLabel = this._data.columns[2] || 'y';
let xLabel = this._config.xAxisLabel || this._data.columns[1] || 'x';
let yLabel = this._config.yAxisLabel || this._data.columns[2] || 'y';
let options = {
scales: {
xAxes: [{

View File

@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import LineChart from './lineChart.component';
import * as nls from 'vs/nls';
const properties: IJSONSchema = {
properties: {
dataType: {
@@ -23,6 +23,6 @@ const properties: IJSONSchema = {
}
};
export const lineSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
export const lineSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('line', '', lineSchema, LineChart);

View File

@@ -4,14 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import ScatterChart from './scatterChart.component';
const properties: IJSONSchema = {
};
const scatterSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
const scatterSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('scatter', '', scatterSchema, ScatterChart);

View File

@@ -15,8 +15,8 @@ export default class TimeSeriesChart extends LineChart {
protected _defaultConfig = defaultTimeSeriesConfig;
protected addAxisLabels(): void {
let xLabel = this.getLabels()[1] || 'x';
let yLabel = this.getLabels()[2] || 'y';
let xLabel = this._config.xAxisLabel || this.getLabels()[1] || 'x';
let yLabel = this._config.yAxisLabel || this.getLabels()[2] || 'y';
let options = {
scales: {

View File

@@ -4,14 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { mixin, clone } from 'vs/base/common/objects';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { registerInsight } from 'sql/platform/dashboard/common/insightRegistry';
import { chartInsightSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.contribution';
import { properties as BarChartSchema } from 'sql/parts/dashboard/widgets/insights/views/charts/types/barChart.contribution';
import TimeSeriesChart from './timeSeriesChart.component';
const properties: IJSONSchema = {
};
const timeSeriesSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
const timeSeriesSchema = mixin(clone(BarChartSchema), properties) as IJSONSchema;
registerInsight('timeSeries', '', timeSeriesSchema, TimeSeriesChart);

View File

@@ -14,4 +14,4 @@ let countInsightSchema: IJSONSchema = {
description: nls.localize('countInsightDescription', 'For each column in a resultset, displays the value in row 0 as a count followed by the column name. Supports "1 Healthy", "3 Unhealthy" for example, where "Healthy" is the column name and 1 is the value in row 1 cell 1')
};
registerInsight('count', '', countInsightSchema, CountInsight);
registerInsight('count', '', countInsightSchema, CountInsight);

View File

@@ -76,5 +76,4 @@ export default class ImageInsight implements IInsightsView, OnInit {
// 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(' ')));
}
}
}

View File

@@ -28,4 +28,4 @@ let imageInsightSchema: IJSONSchema = {
}
};
registerInsight('image', '', imageInsightSchema, ImageInsight);
registerInsight('image', '', imageInsightSchema, ImageInsight);

View File

@@ -104,5 +104,5 @@ export const properties: Array<ProviderProperties> = [
]
}
]
},
}
];

View File

@@ -4,18 +4,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div #parent style="position: absolute; height: 100%; width: 100%; margin: 10px 0px 10px 0px; ">
<div [style.margin-right.px]="_clipped ? 30 : 0" [style.width]="_clipped ? 94 + '%' : '100%'" style="overflow: hidden">
<span #child style="white-space : nowrap; width: fit-content">
<ng-template ngFor let-item [ngForOf]="properties">
<span style="margin-left: 10px; display: inline-block;">
<div style="font-size: 11px; font-weight: lighter">{{item.displayName}}</div>
<div>{{item.value}}</div>
</span>
</ng-template>
</span>
</div>
<span *ngIf="_clipped" style="position: absolute; right: 0; top: 0; padding-top: 5px; padding-right: 14px; z-index: 2">
...
</span>
<div #parent style="position: absolute; height: 100%; width: 100%;">
<div [style.margin-right.px]="_clipped ? 30 : 0" [style.width]="_clipped ? 94 + '%' : '100%'" style="overflow: hidden">
<span #child style="white-space : nowrap; width: fit-content">
<ng-template ngFor let-item [ngForOf]="properties">
<span style="margin-left: 10px; display: inline-block;">
<div style="font-size: 11px; font-weight: lighter">{{item.displayName}}</div>
<div>{{item.value}}</div>
</span>
</ng-template>
</span>
</div>
<span *ngIf="_clipped" style="position: absolute; right: 0; top: 0; padding-top: 5px; padding-right: 14px; z-index: 2">
...
</span>
</div>

View File

@@ -3,22 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ElementRef, ViewChild } from '@angular/core';
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { error } from 'sql/base/common/log';
import { properties } from './propertiesJson';
import { IDashboardRegistry, Extensions as DashboardExtensions } from 'sql/platform/dashboard/common/dashboardRegistry';
import { DatabaseInfo, ServerInfo } from 'data';
import { IDisposable } from 'vs/base/common/lifecycle';
import { EventType, addDisposableListener } from 'vs/base/browser/dom';
import * as types from 'vs/base/common/types';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
export interface PropertiesConfig {
properties: Array<Property>;
@@ -47,6 +46,8 @@ export interface Property {
default?: string;
}
const dashboardRegistry = Registry.as<IDashboardRegistry>(DashboardExtensions.DashboardContributions);
export interface DisplayProperty {
displayName: string;
value: string;
@@ -56,11 +57,10 @@ export interface DisplayProperty {
selector: 'properties-widget',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/properties/propertiesWidget.component.html'))
})
export class PropertiesWidgetComponent extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
export class PropertiesWidgetComponent extends DashboardWidget implements IDashboardWidget, OnInit {
private _connection: ConnectionManagementInfo;
private _databaseInfo: DatabaseInfo;
private _clipped: boolean;
private _disposables: Array<IDisposable> = [];
private properties: Array<DisplayProperty>;
private _hasInit = false;
@@ -83,21 +83,17 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb
ngOnInit() {
this._hasInit = true;
this._disposables.push(addDisposableListener(window, EventType.RESIZE, () => this.handleClipping()));
this._register(addDisposableListener(window, EventType.RESIZE, () => this.handleClipping()));
this._changeRef.detectChanges();
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
}
public refresh(): void {
this.init();
}
private init(): void {
this._connection = this._bootstrap.connectionManagementService.connectionInfo;
this._disposables.push(toDisposableSubscription(this._bootstrap.adminService.databaseInfo.subscribe(data => {
this._register(toDisposableSubscription(this._bootstrap.adminService.databaseInfo.subscribe(data => {
this._databaseInfo = data;
this._changeRef.detectChanges();
this.parseProperties();
@@ -128,29 +124,13 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb
let config = <PropertiesConfig>this._config.widget['properties-widget'];
propertyArray = config.properties;
} else {
let propertiesConfig: Array<ProviderProperties> = properties;
// ensure we have a properties file
if (!Array.isArray(propertiesConfig)) {
this.consoleError('Could not load properties JSON');
let providerProperties = dashboardRegistry.getProperties(provider as string);
if (!providerProperties) {
this.consoleError('No property definitions found for provider', provider);
return;
}
// filter the properties provided based on provider name
let providerPropertiesArray = propertiesConfig.filter((item) => {
return item.provider === provider;
});
// Error handling on provider
if (providerPropertiesArray.length === 0) {
this.consoleError('Could not locate properties for provider: ', provider);
return;
} else if (providerPropertiesArray.length > 1) {
this.consoleError('Found multiple property definitions for provider ', provider);
return;
}
let providerProperties = providerPropertiesArray[0];
let flavor: FlavorProperties;
// find correct flavor
@@ -262,4 +242,4 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb
private consoleError(message?: any, ...optionalParams: any[]): void {
error(message, optionalParams);
}
}
}

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.
*--------------------------------------------------------------------------------------------*/
tasks-widget .tile-container {
position: relative;
display: flex;
flex-flow: column;
flex-wrap: wrap;
}
tasks-widget .task-tile {
cursor: pointer;
display: flex;
flex-flow: row;
align-items: center;
text-align: center;
margin-top: 10px;
margin-left: 18px;
}
tasks-widget .task-tile:last-of-type {
margin-right: 10px;
}
tasks-widget .task-tile > div {
flex: 1 1 auto;
display: flex;
flex-flow: column;
align-items: center;
}
tasks-widget .task-tile .icon {
padding: 15px;
}

View File

@@ -4,18 +4,5 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="task-widget">
<ng-template ngFor let-task [ngForOf]="_actions" let-i="index">
<div (click)="runTask(task)"
style="position: absolute; cursor: pointer; display: flex; flex-flow: row; align-items: center; text-align: center"
class="task-tile"
[style.height.px]="_size"
[style.width.px]="_size"
[style.transform]="calculateTransform(i)">
<div style="flex: 1 1 auto; display: flex; flex-flow: column; align-items: center">
<span [ngClass]="['icon', task.icon]" style="padding: 15px"></span>
<div>{{task.label}}</div>
</div>
</div>
</ng-template>
</div>
<div #container style="position: absolute; height: 100%; width: 100%">
</div>

View File

@@ -3,20 +3,20 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!./media/taskWidget';
/* Node Modules */
import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { Component, Inject, forwardRef, ChangeDetectorRef, ViewChild, OnInit, ElementRef } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
/* SQL imports */
import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { ITaskRegistry, Extensions, ActionICtor } from 'sql/platform/tasks/taskRegistry';
import { ITaskRegistry, Extensions, TaskAction } from 'sql/platform/tasks/taskRegistry';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { ITaskActionContext } from 'sql/workbench/common/actions';
import { BaseActionContext } from 'sql/workbench/common/actions';
/* VS imports */
import { IDisposable } from 'vs/base/common/lifecycle';
import * as themeColors from 'vs/workbench/common/theme';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant, ICssStyleCollector, ITheme } from 'vs/platform/theme/common/themeService';
@@ -24,6 +24,11 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { Action } from 'vs/base/common/actions';
import Severity from 'vs/base/common/severity';
import * as nls from 'vs/nls';
import * as types from 'vs/base/common/types';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { $, Builder } from 'vs/base/browser/builder';
import * as DOM from 'vs/base/browser/dom';
interface IConfig {
tasks: Array<Object>;
@@ -33,14 +38,14 @@ interface IConfig {
selector: 'tasks-widget',
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/widgets/tasks/tasksWidget.component.html'))
})
export class TasksWidget extends DashboardWidget implements IDashboardWidget, OnInit, OnDestroy {
private _size: number = 100;
private _margins: number = 10;
private _rows: number = 2;
private _isAzure = false;
private _themeDispose: IDisposable;
private _actions: Array<Action> = [];
export class TasksWidget extends DashboardWidget implements IDashboardWidget, OnInit {
private _size: number = 98;
private _tasks: Array<TaskAction> = [];
private _profile: IConnectionProfile;
private _scrollableElement: ScrollableElement;
private $container: Builder;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
constructor(
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
@@ -52,35 +57,77 @@ export class TasksWidget extends DashboardWidget implements IDashboardWidget, On
this._profile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
let registry = Registry.as<ITaskRegistry>(Extensions.TaskContribution);
let tasksConfig = <IConfig>Object.values(this._config.widget)[0];
let connInfo = this._bootstrap.connectionManagementService.connectionInfo;
let taskIds: Array<string>;
if (tasksConfig.tasks) {
Object.keys(tasksConfig.tasks).forEach((item) => {
if (registry.idToCtorMap[item]) {
let ctor = registry.idToCtorMap[item];
this._actions.push(this._bootstrap.instantiationService.createInstance(ctor, ctor.ID, ctor.LABEL, ctor.ICON));
} else {
this._bootstrap.messageService.show(Severity.Warning, nls.localize('missingTask', 'Could not find task {0}; are you missing an extension?', item));
}
});
taskIds = Object.keys(tasksConfig.tasks);
} else {
let actions = Object.values(registry.idToCtorMap).map((item: ActionICtor) => {
let action = this._bootstrap.instantiationService.createInstance(item, item.ID, item.LABEL, item.ICON);
if (this._bootstrap.CapabilitiesService.isFeatureAvailable(action, connInfo)) {
return action;
} else {
return undefined;
}
});
this._actions = actions.filter(x => x !== undefined);
taskIds = registry.ids;
}
this._isAzure = connInfo.serverInfo.isCloud;
let ctorMap = registry.idToCtorMap;
this._tasks = taskIds.map(id => {
let ctor = ctorMap[id];
if (ctor) {
let action = this._bootstrap.instantiationService.createInstance(ctor, ctor.ID, ctor.LABEL, ctor.ICON);
if (this._bootstrap.capabilitiesService.isFeatureAvailable(action, this._bootstrap.connectionManagementService.connectionInfo)) {
return action;
}
} else {
this._bootstrap.messageService.show(Severity.Warning, nls.localize('missingTask', 'Could not find task {0}; are you missing an extension?', id));
}
return undefined;
}).filter(a => !types.isUndefinedOrNull(a));
}
ngOnInit() {
this._themeDispose = registerThemingParticipant(this.registerThemeing);
this._register(registerThemingParticipant(this.registerThemeing));
this._computeContainer();
this._tasks.map(a => {
this.$container.append(this._createTile(a));
});
this._scrollableElement = this._register(new ScrollableElement(this.$container.getHTMLElement(), {
horizontal: ScrollbarVisibility.Auto,
vertical: ScrollbarVisibility.Hidden,
scrollYToX: true,
useShadows: false
}));
this._scrollableElement.onScroll(e => {
this.$container.getHTMLElement().style.right = e.scrollLeft + 'px';
});
(this._container.nativeElement as HTMLElement).appendChild(this._scrollableElement.getDomNode());
// Update scrollbar
this._scrollableElement.setScrollDimensions({
width: DOM.getContentWidth(this._container.nativeElement),
scrollWidth: DOM.getContentWidth(this.$container.getHTMLElement()) + 18 // right padding
});
}
private _computeContainer(): void {
let height = DOM.getContentHeight(this._container.nativeElement);
let tilesHeight = Math.floor(height / (this._size + 10));
let width = (this._size + 18) * Math.ceil(this._tasks.length / tilesHeight);
if (!this.$container) {
this.$container = $('.tile-container');
this._register(this.$container);
}
this.$container.style('height', height + 'px').style('width', width + 'px');
}
private _createTile(action: TaskAction): HTMLElement {
let label = $('div').safeInnerHtml(action.label);
let icon = $('span.icon').addClass(action.icon);
let innerTile = $('div').append(icon).append(label);
let tile = $('div.task-tile').style('height', this._size + 'px').style('width', this._size + 'px');
tile.append(innerTile);
tile.on(DOM.EventType.CLICK, () => this.runTask(action));
return tile.getHTMLElement();
}
private registerThemeing(theme: ITheme, collector: ICssStyleCollector) {
@@ -88,30 +135,26 @@ export class TasksWidget extends DashboardWidget implements IDashboardWidget, On
let sideBarColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND);
if (contrastBorder) {
let contrastBorderString = contrastBorder.toString();
collector.addRule(`.task-widget .task-tile { border: 1px solid ${contrastBorderString} }`);
collector.addRule(`tasks-widget .task-tile { border: 1px solid ${contrastBorderString} }`);
} else {
let sideBarColorString = sideBarColor.toString();
collector.addRule(`.task-widget .task-tile { background-color: ${sideBarColorString} }`);
collector.addRule(`tasks-widget .task-tile { background-color: ${sideBarColorString} }`);
}
}
ngOnDestroy() {
this._themeDispose.dispose();
}
//tslint:disable-next-line
private calculateTransform(index: number): string {
let marginy = (1 + (index % this._rows)) * this._margins;
let marginx = (1 + (Math.floor(index / 2))) * this._margins;
let posx = (this._size * (Math.floor(index / 2))) + marginx;
let posy = (this._size * (index % this._rows)) + marginy;
return 'translate(' + posx + 'px, ' + posy + 'px)';
}
public runTask(task: Action) {
let context: ITaskActionContext = {
let context: BaseActionContext = {
profile: this._profile
};
task.run(context);
}
public layout(): void {
this._computeContainer();
// Update scrollbar
this._scrollableElement.setScrollDimensions({
width: DOM.getContentWidth(this._container.nativeElement),
scrollWidth: DOM.getContentWidth(this.$container.getHTMLElement()) + 18 // right padding
});
}
}

View File

@@ -22,7 +22,7 @@
</div>
<div class="input-divider" #backupTypeContainer>
</div>
<div class="input-divider" #copyOnlyContainer>{{copyOnlyLabel}}
<div class="input-divider check" #copyOnlyContainer>
</div>
<div class="dialog-label">
{{backupDeviceLabel}}
@@ -33,134 +33,128 @@
</div>
<table class="backup-path-table">
<tr>
<td>
<td style="padding-left: 0px; padding-right: 0px;">
<div class="backup-path-button" #addPathContainer></div>
</td>
<td style="padding-right:0px">
<td>
<div class="backup-path-button" #removePathContainer></div>
</td>
</tr>
</table>
<!-- Advanced Configuration -->
<div class="monaco-split-view vertical advanced-main-header">
<div class="split-view-view" >
<!-- TODO: should this be shown in a new page instead of collapsible view? -->
<div class="header collapsible collapsed" name="advanced-title" aria-expanded="false" style="height: 22px; background-color: rgba(128, 128, 128, 0.2);" (click)="onAdvancedClick()" #advancedHeader>
<div class="title">{{advancedConfigurationLabel}}
<div class="advanced-main-header" #advancedOptionContainer>
<div class="advanced-main-body" #advancedOptionBodyContainer>
<!-- Compression -->
<div class="dialog-label advanced-header">
{{compressionLabel}}
</div>
<div class="indent">
<div class="dialog-label">
{{setBackupCompressionLabel}}
</div>
<div class="dialog-label" #compressionContainer>
</div>
</div>
<div class="body collapsible" name="advanced-body" style="height: calc(100% - 22px); display: none" #advancedBody>
<!-- Compression -->
<div class="dialog-label advanced-header" style="padding-top: 15px;">
{{compressionLabel}}
</div>
<div class="indent">
<div class="dialog-label">
{{setBackupCompressionLabel}}
</div>
<div class="dialog-label" #compressionContainer>
</div>
</div>
<!-- Encryption -->
<div class="dialog-label advanced-header">
<!-- Encryption -->
<div class="dialog-label advanced-header">
{{encryptionLabel}}
</div>
<div class="indent">
<div class="option check" #encryptCheckContainer>
</div>
<div class="indent">
<div class="option" #encryptCheckContainer>Encrypt backup
<div class="option" #encryptWarningContainer>
<div class="icon warning">
</div>
<div class="option" #encryptWarningContainer>
<div class="icon warning">
</div>
<div class="warning-message">
{{noEncryptorWarning}}
</div>
</div>
<div #encryptContainer>
<div class="dialog-label">
{{algorithmLabel}}
</div>
<div class="dialog-label" #algorithmContainer>
</div>
<div class="dialog-label">
{{certificateOrAsymmetricKeyLabel}}
</div>
<div class="dialog-label" #encryptorContainer>
</div>
<div class="warning-message">
{{noEncryptorWarning}}
</div>
</div>
<!-- Overwrite media -->
<div class="dialog-label advanced-header">
{{mediaLabel}}
</div>
<div class="radio-indent">
<div class="option">
<input type="radio" name=media-option value="no_format" [checked]="!isFormatChecked" (change)="onChangeMediaFormat()" [disabled]="isEncryptChecked">{{mediaOptionLabel}}
</div>
<div style="margin-left:15px">
<div class="option">
<input type="radio" name=existing-media value="append" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked">{{existingMediaAppendLabel}}
</div>
<div class="option">
<input type="radio" name=existing-media value="overwrite" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked">{{existingMediaOverwriteLabel}}
</div>
</div>
<div class="option">
<input type="radio" name=media-option value="format" [checked]="isFormatChecked" (change)="onChangeMediaFormat()">{{mediaOptionFormatLabel}}
</div>
<div style="margin-left: 22px">
<div class="dialog-label">
{{newMediaSetNameLabel}}
</div>
<div class="dialog-label" #mediaName>
</div>
<div class="dialog-label">
{{newMediaSetDescriptionLabel}}
</div>
<div class="dialog-label" #mediaDescription>
</div>
</div>
</div>
<!-- Transaction log -->
<div class="dialog-label advanced-header">
{{transactionLogLabel}}
</div>
<div class="radio-indent">
<div class="option">
<input type="radio" name=t-log value="truncate" [checked]="isTruncateChecked" (change)="onChangeTlog()" [disabled]="disableTlog">Truncate the transaction log
</div>
<div class="option">
<input type="radio" name=t-log value="taillog" [checked]="isTaillogChecked" (change)="onChangeTlog()" [disabled]="disableTlog">Backup the tail of the log
</div>
</div>
<!-- Reliability -->
<div class="dialog-label advanced-header">
{{reliabilityLabel}}
</div>
<div class="indent">
<div class="option" #checksumContainer>{{checksumContainerLabel}}
</div>
<div class="option" #verifyContainer>{{verifyContainerLabel}}
</div>
<div class="option" #continueOnErrorContainer>{{continueOnErrorContainerLabel}}
</div>
</div>
<!-- Backup expiration -->
<div class="dialog-label advanced-header">
{{expirationLabel}}
</div>
<div class="indent">
<div #encryptContainer>
<div class="dialog-label">
{{setBackupRetainDaysLabel}}
{{algorithmLabel}}
</div>
<div class="dialog-label" #algorithmContainer>
</div>
<div class="dialog-label">
<div #backupDaysContainer></div>
{{certificateOrAsymmetricKeyLabel}}
</div>
<div class="dialog-label" #encryptorContainer>
</div>
</div>
</div>
<!-- Overwrite media -->
<div class="dialog-label advanced-header">
{{mediaLabel}}
</div>
<div class="radio-indent">
<div class="option">
<input type="radio" name=media-option value="no_format" [checked]="!isFormatChecked" (change)="onChangeMediaFormat()" [disabled]="isEncryptChecked">{{mediaOptionLabel}}
</div>
<div style="margin-left:15px">
<div class="option">
<input type="radio" name=existing-media value="append" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked">{{existingMediaAppendLabel}}
</div>
<div class="option">
<input type="radio" name=existing-media value="overwrite" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked">{{existingMediaOverwriteLabel}}
</div>
</div>
<div class="option">
<input type="radio" name=media-option value="format" [checked]="isFormatChecked" (change)="onChangeMediaFormat()">{{mediaOptionFormatLabel}}
</div>
<div style="margin-left: 22px">
<div class="dialog-label">
{{newMediaSetNameLabel}}
</div>
<div class="dialog-label" #mediaName>
</div>
<div class="dialog-label">
{{newMediaSetDescriptionLabel}}
</div>
<div class="dialog-label" #mediaDescription>
</div>
</div>
</div>
<!-- Transaction log -->
<div class="dialog-label advanced-header">
{{transactionLogLabel}}
</div>
<div class="radio-indent">
<div class="option">
<input type="radio" name=t-log value="truncate" [checked]="isTruncateChecked" (change)="onChangeTlog()" [disabled]="disableTlog">Truncate the transaction log
</div>
<div class="option">
<input type="radio" name=t-log value="taillog" [checked]="isTaillogChecked" (change)="onChangeTlog()" [disabled]="disableTlog">Backup the tail of the log
</div>
</div>
<!-- Reliability -->
<div class="dialog-label advanced-header">
{{reliabilityLabel}}
</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">
{{expirationLabel}}
</div>
<div class="indent">
<div class="dialog-label">
{{setBackupRetainDaysLabel}}
</div>
<div class="dialog-label">
<div #backupDaysContainer></div>
</div>
</div>
</div>

View File

@@ -6,27 +6,30 @@
import 'vs/css!sql/parts/disasterRecovery/backup/media/backupDialog';
import { ElementRef, Component, Inject, forwardRef, ViewChild, ChangeDetectorRef } from '@angular/core';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { ListBox } from 'sql/base/browser/ui/listBox/listBox';
import { Button } from 'sql/base/browser/ui/button/button';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { ListBox } from 'sql/base/browser/ui/listBox/listBox';
import { ModalFooterStyle } from 'sql/base/browser/ui/modal/modal';
import { attachListBoxStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'sql/common/theme/styler';
import { CategoryView } from 'sql/base/browser/ui/modal/optionsDialog';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { SplitView } from 'sql/base/browser/ui/splitview/splitview';
import { attachButtonStyler, attachListBoxStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'sql/common/theme/styler';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import * as BackupConstants from 'sql/parts/disasterRecovery/backup/constants';
import { IDisasterRecoveryService, IDisasterRecoveryUiService, TaskExecutionMode } from 'sql/parts/disasterRecovery/common/interfaces';
import FileValidationConstants = require('sql/parts/fileBrowser/common/fileValidationServiceConstants');
import { FileBrowserDialog } from 'sql/parts/fileBrowser/fileBrowserDialog';
import { DashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { Button } from 'vs/base/browser/ui/button/button';
import * as lifecycle from 'vs/base/common/lifecycle';
import { attachButtonStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import * as types from 'vs/base/common/types';
import * as strings from 'vs/base/common/strings';
export const BACKUP_SELECTOR: string = 'backup-component';
@@ -87,8 +90,6 @@ export class BackupComponent {
@ViewChild('encryptorContainer', { read: ElementRef }) encryptorElement;
@ViewChild('mediaName', { read: ElementRef }) mediaNameElement;
@ViewChild('mediaDescription', { read: ElementRef }) mediaDescriptionElement;
@ViewChild('advancedBody', { read: ElementRef }) advancedBodyElement;
@ViewChild('advancedHeader', { read: ElementRef }) advancedHeaderElement;
@ViewChild('recoveryModelContainer', { read: ElementRef }) recoveryModelElement;
@ViewChild('backupDaysContainer', { read: ElementRef }) backupDaysElement;
@ViewChild('backupButtonContainer', { read: ElementRef }) backupButtonElement;
@@ -105,6 +106,8 @@ export class BackupComponent {
@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;
// tslint:disable:no-unused-variable
private readonly backupNameLabel: string = localize('backup.backupName', 'Backup name');
@@ -141,6 +144,7 @@ export class BackupComponent {
private _disasterRecoveryUiService: IDisasterRecoveryUiService;
private _uri: string;
private _toDispose: lifecycle.IDisposable[] = [];
private _advancedHeaderSize = 32;
private connection: IConnectionProfile;
private databaseName: string;
@@ -148,8 +152,6 @@ export class BackupComponent {
private recoveryModel: string;
private backupEncryptors;
private containsBackupToUrl: boolean;
private fileBrowserDialog: FileBrowserDialog;
private errorMessage: string;
// UI element disable flag
private disableFileComponent: boolean;
@@ -203,7 +205,6 @@ export class BackupComponent {
ngOnInit() {
let self = this;
this.addFooterButtons();
this.recoveryBox = new InputBox(this.recoveryModelElement.nativeElement, this._bootstrapService.contextViewService, { placeholder: this.recoveryModel });
@@ -212,65 +213,55 @@ export class BackupComponent {
this.backupTypeSelectBox.render(this.backupTypeElement.nativeElement);
// Set copy-only check box
this.copyOnlyCheckBox = new Checkbox({
actionClassName: 'backup-checkbox',
title: this.copyOnlyLabel,
isChecked: false,
this.copyOnlyCheckBox = new Checkbox(this.copyOnlyElement.nativeElement, {
label: this.copyOnlyLabel,
checked: false,
onChange: (viaKeyboard) => { }
});
this.copyOnlyElement.nativeElement.appendChild(this.copyOnlyCheckBox.domNode);
// Encryption checkbox
this.encryptCheckBox = new Checkbox({
actionClassName: 'backup-checkbox',
title: 'Encryption',
isChecked: false,
this.encryptCheckBox = new Checkbox(this.encryptElement.nativeElement, {
label: this.encryptionLabel,
checked: false,
onChange: (viaKeyboard) => self.onChangeEncrypt()
});
this.encryptElement.nativeElement.appendChild(this.encryptCheckBox.domNode);
// Verify backup checkbox
this.verifyCheckBox = new Checkbox({
actionClassName: 'backup-checkbox',
title: 'Verify',
isChecked: false,
this.verifyCheckBox = new Checkbox(this.verifyElement.nativeElement, {
label: this.verifyContainerLabel,
checked: false,
onChange: (viaKeyboard) => { }
});
this.verifyElement.nativeElement.appendChild(this.verifyCheckBox.domNode);
// Perform checksum checkbox
this.checksumCheckBox = new Checkbox({
actionClassName: 'backup-checkbox',
title: 'Perform checksum',
isChecked: false,
this.checksumCheckBox = new Checkbox(this.checksumElement.nativeElement, {
label: this.checksumContainerLabel,
checked: false,
onChange: (viaKeyboard) => { }
});
this.checksumElement.nativeElement.appendChild(this.checksumCheckBox.domNode);
// Continue on error checkbox
this.continueOnErrorCheckBox = new Checkbox({
actionClassName: 'backup-checkbox',
title: 'Continue on error',
isChecked: false,
this.continueOnErrorCheckBox = new Checkbox(this.continueOnErrorElement.nativeElement, {
label: this.continueOnErrorContainerLabel,
checked: false,
onChange: (viaKeyboard) => { }
});
this.continueOnErrorElement.nativeElement.appendChild(this.continueOnErrorCheckBox.domNode);
// Set backup name
this.backupNameBox = new InputBox(this.backupNameElement.nativeElement, this._bootstrapService.contextViewService);
this.backupNameBox.focus();
// Set backup path list
this.pathListBox = new ListBox([], '', this._bootstrapService.contextViewService);
this.pathListBox = new ListBox([], '', this._bootstrapService.contextViewService, this._bootstrapService.clipboardService);
this.pathListBox.render(this.pathElement.nativeElement);
// Set backup path add/remove buttons
this.addPathButton = new Button(this.addPathElement.nativeElement);
this.addPathButton.label = '+';
this.addPathButton.title = localize('addFile', 'Add a file');
this.removePathButton = new Button(this.removePathElement.nativeElement);
this.removePathButton.label = '-';
this.removePathButton.title = localize('removeFile', 'Remove files');
// Set compression
this.compressionSelectBox = new SelectBox(this.compressionOptions, this.compressionOptions[0]);
@@ -312,7 +303,7 @@ export class BackupComponent {
}
});
// disable elements
// Disable elements
this.recoveryBox.disable();
this.mediaNameBox.disable();
this.mediaDescriptionBox.disable();
@@ -321,6 +312,17 @@ export class BackupComponent {
this.updateTheme();
}
ngAfterViewInit() {
// Set category view for advanced options. This should be defined in ngAfterViewInit so that it correctly calculates the text height after data binding.
var splitview = new SplitView(this.advancedOptionElement.nativeElement);
var advancedBodySize = DOM.getTotalHeight(this.advancedOptionBodyElement.nativeElement);
var categoryView = new CategoryView(this.advancedConfigurationLabel, this.advancedOptionBodyElement.nativeElement, true, advancedBodySize, this._advancedHeaderSize);
splitview.addView(categoryView);
splitview.layout(advancedBodySize + this._advancedHeaderSize);
this._disasterRecoveryUiService.onShowBackupDialog();
}
private onGetBackupConfigInfo(param: DashboardComponentParams) {
// Show spinner
this.showSpinner();
@@ -366,26 +368,26 @@ export class BackupComponent {
// Set script footer button
this.scriptButton = new Button(this.scriptButtonElement.nativeElement);
this.scriptButton.label = localize('script', 'Script');
this._toDispose.push(this.addButtonClickHandler(this.scriptButton, () => this.onScript()));
this.addButtonClickHandler(this.scriptButton, () => this.onScript());
this._toDispose.push(attachButtonStyler(this.scriptButton, this._bootstrapService.themeService));
this.scriptButton.enabled = false;
// Set backup footer button
this.backupButton = new Button(this.backupButtonElement.nativeElement);
this.backupButton.label = localize('backup', 'Backup');
this._toDispose.push(this.addButtonClickHandler(this.backupButton, () => this.onOk()));
this.addButtonClickHandler(this.backupButton, () => this.onOk());
this._toDispose.push(attachButtonStyler(this.backupButton, this._bootstrapService.themeService));
this.backupEnabled = false;
// Set cancel footer button
this.cancelButton = new Button(this.cancelButtonElement.nativeElement);
this.cancelButton.label = localize('cancel', 'Cancel');
this._toDispose.push(this.addButtonClickHandler(this.cancelButton, () => this.onCancel()));
this.addButtonClickHandler(this.cancelButton, () => this.onCancel());
this._toDispose.push(attachButtonStyler(this.cancelButton, this._bootstrapService.themeService));
}
private initialize(isMetadataPopulated: boolean): void {
this.databaseName = this.connection.databaseName;
this.selectedBackupComponent = BackupConstants.labelDatabase;
this.backupPathTypePairs = {};
@@ -475,7 +477,6 @@ export class BackupComponent {
this.backupRetainDaysBox.value = '0';
this.algorithmSelectBox.setOptions(this.encryptionAlgorithms, 0);
this.selectedInitOption = this.existingMediaOptions[0];
this.collapseAdvancedOptions();
this.containsBackupToUrl = false;
this.pathListBox.setValidation(true);
@@ -498,15 +499,10 @@ export class BackupComponent {
this._toDispose.push(attachInputBoxStyler(this.mediaNameBox, this._bootstrapService.themeService));
this._toDispose.push(attachInputBoxStyler(this.mediaDescriptionBox, this._bootstrapService.themeService));
this._toDispose.push(attachInputBoxStyler(this.backupRetainDaysBox, this._bootstrapService.themeService));
this._toDispose.push(attachCheckboxStyler(this.copyOnlyCheckBox, this._bootstrapService.themeService));
this._toDispose.push(attachCheckboxStyler(this.encryptCheckBox, this._bootstrapService.themeService));
this._toDispose.push(attachCheckboxStyler(this.verifyCheckBox, this._bootstrapService.themeService));
this._toDispose.push(attachCheckboxStyler(this.checksumCheckBox, this._bootstrapService.themeService));
this._toDispose.push(attachCheckboxStyler(this.continueOnErrorCheckBox, this._bootstrapService.themeService));
this._toDispose.push(this.backupTypeSelectBox.onDidSelect(selected => this.onBackupTypeChanged()));
this._toDispose.push(this.addButtonClickHandler(this.addPathButton, () => this.onAddClick()));
this._toDispose.push(this.addButtonClickHandler(this.removePathButton, () => this.onRemoveClick()));
this.addButtonClickHandler(this.addPathButton, () => this.onAddClick());
this.addButtonClickHandler(this.removePathButton, () => this.onRemoveClick());
this._toDispose.push(this.mediaNameBox.onDidChange(mediaName => {
this.mediaNameChanged(mediaName);
}));
@@ -527,15 +523,22 @@ export class BackupComponent {
footerHtmlElement.style.borderTopColor = ModalFooterStyle.borderTopColor;
}
private addButtonClickHandler(button: Button, handler: () => void): lifecycle.IDisposable {
private addButtonClickHandler(button: Button, handler: () => void) {
if (button && handler) {
return DOM.addDisposableListener(button.getElement(), DOM.EventType.CLICK, () => {
button.addListener(DOM.EventType.CLICK, () => {
if (button.enabled) {
handler();
}
});
} else {
return undefined;
button.addListener(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
var event = new StandardKeyboardEvent(e);
if (button.enabled && event.keyCode === KeyCode.Enter) {
handler();
event.preventDefault();
event.stopPropagation();
}
});
}
}
@@ -587,7 +590,7 @@ export class BackupComponent {
this.isFormatChecked = !this.isFormatChecked;
this.enableMediaInput(this.isFormatChecked);
if (this.isFormatChecked) {
if (DialogHelper.isNullOrWhiteSpace(this.mediaNameBox.value)) {
if (strings.isFalsyOrWhitespace(this.mediaNameBox.value)) {
this.backupEnabled = false;
this.backupButton.enabled = false;
this.mediaNameBox.showMessage({ type: MessageType.ERROR, content: this.mediaNameRequiredError });
@@ -603,31 +606,6 @@ export class BackupComponent {
this.scriptButton.enabled = value;
}
private onAdvancedClick(): void {
if (this.advancedHeaderElement.nativeElement.style['aria-expanded']) {
// collapse
this.collapseAdvancedOptions();
} else {
// expand
this.expandAdvancedOptions();
}
this.detectChange();
}
private collapseAdvancedOptions() {
this.advancedHeaderElement.nativeElement.className = 'header collapsible collapsed';
this.advancedBodyElement.nativeElement.style = 'display: none';
this.advancedHeaderElement.nativeElement.style['aria-expanded'] = false;
}
private expandAdvancedOptions() {
this.advancedHeaderElement.nativeElement.className = 'header collapsible';
this.advancedBodyElement.nativeElement.style = 'display: inline';
this.advancedHeaderElement.nativeElement.style['aria-expanded'] = true;
}
private onBackupTypeChanged(): void {
if (this.getSelectedBackupType() === BackupConstants.labelDifferential) {
this.copyOnlyCheckBox.checked = false;
@@ -740,8 +718,8 @@ export class BackupComponent {
}
private setDefaultBackupName(): void {
let utc = new Date().toJSON().slice(0, 19);
if (this.backupNameBox) {
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;
}
}
@@ -908,7 +886,7 @@ export class BackupComponent {
continueAfterError: this.continueOnErrorCheckBox.checked,
logTruncation: this.isTruncateChecked,
tailLogBackup: this.isTaillogChecked,
retainDays: DialogHelper.isNullOrWhiteSpace(this.backupRetainDaysBox.value) ? 0 : this.backupRetainDaysBox.value,
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),

View File

@@ -20,16 +20,20 @@
height: calc(100% - 15px)
}
.advanced-main-header {
.backup-dialog .advanced-main-header {
padding-top: 20px;
}
.advanced-header {
.backup-dialog .advanced-header {
padding-top: 15px;
font-size: 14px;
}
input[type="radio"] {
.backup-dialog input[type="checkbox"] {
margin-left: 0px;
}
.backup-dialog input[type="radio"] {
margin-top: -2px;
vertical-align: middle;
}
@@ -39,7 +43,7 @@ input[type="radio"] {
}
.backup-dialog .radio-indent {
margin-left: 5px;
margin-left: 2px;
}
.backup-dialog .option {
@@ -47,6 +51,15 @@ input[type="radio"] {
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;

View File

@@ -103,6 +103,13 @@ export class DisasterRecoveryUiService implements IDisasterRecoveryUiService {
});
}
public onShowBackupDialog() {
let backupDialog = this._backupDialogs[this._currentProvider];
if (backupDialog) {
backupDialog.setFocusableElements();
}
}
public closeBackup() {
let self = this;
let backupDialog = self._backupDialogs[self._currentProvider];

View File

@@ -42,6 +42,11 @@ export interface IDisasterRecoveryUiService {
* Close backup wizard
*/
closeBackup();
/**
* After the backup dialog is rendered, run Modal methods to set focusable elements, etc.
*/
onShowBackupDialog();
}
export const IDisasterRecoveryService = createDecorator<IDisasterRecoveryService>(SERVICE_ID);

View File

@@ -6,18 +6,19 @@
'use strict';
import 'vs/css!./media/restoreDialog';
import { Builder, $ } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import Event, { Emitter } from 'vs/base/common/event';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Widget } from 'vs/base/browser/ui/widget';
import { MessageType, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachButtonStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { localize } from 'vs/nls';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Button } from 'sql/base/browser/ui/button/button';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { InputBox, OnLoseFocusParams } from 'sql/base/browser/ui/inputBox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
@@ -27,7 +28,7 @@ import { Table } from 'sql/base/browser/ui/table/table';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { attachModalDialogStyler, attachTableStyler, attachInputBoxStyler, attachSelectBoxStyler, attachEditableDropdownStyler } from 'sql/common/theme/styler';
import { attachButtonStyler, attachModalDialogStyler, attachTableStyler, attachInputBoxStyler, attachSelectBoxStyler, attachEditableDropdownStyler } from 'sql/common/theme/styler';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { ServiceOptionType } from 'sql/parts/connection/common/connectionManagement';
import * as BackupConstants from 'sql/parts/disasterRecovery/backup/constants';
@@ -38,6 +39,7 @@ import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { TabbedPanel, PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel';
import * as DOM from 'vs/base/browser/dom';
import * as data from 'data';
import * as strings from 'vs/base/common/strings';
interface FileListElement {
logicalFileName: string;
@@ -59,6 +61,7 @@ export class RestoreDialog extends Modal {
private _backupFileTitle: string;
private _ownerUri: string;
private _databaseDropdown: Dropdown;
private _isBackupFileCheckboxChanged: boolean;
// General options
private _filePathInputBox: InputBox;
@@ -390,6 +393,44 @@ export class RestoreDialog extends Modal {
this._restoreFromSelectBox.hideMessage();
}
});
this._restorePlanTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
this._destinationRestoreToInputBox.isEnabled() ? this._destinationRestoreToInputBox.focus() : this._databaseDropdown.focus();
e.stopImmediatePropagation();
} else if (event.equals(KeyCode.Tab)) {
this.focusOnFirstEnabledFooterButton();
e.stopImmediatePropagation();
}
});
this._fileListTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
if ((<InputBox>this._optionsMap[this._relocatedLogFileFolderOption]).isEnabled()) {
(<InputBox>this._optionsMap[this._relocatedLogFileFolderOption]).focus();
} else if ((<InputBox>this._optionsMap[this._relocatedDataFileFolderOption]).isEnabled()) {
(<InputBox>this._optionsMap[this._relocatedDataFileFolderOption]).focus();
} else {
(<Checkbox>this._optionsMap[this._relocateDatabaseFilesOption]).focus();
}
e.stopImmediatePropagation();
} else if (event.equals(KeyCode.Tab)) {
this.focusOnFirstEnabledFooterButton();
e.stopImmediatePropagation();
}
});
}
private focusOnFirstEnabledFooterButton() {
if (this._scriptButton.enabled) {
this._scriptButton.focus();
} else if (this._restoreButton.enabled) {
this._restoreButton.focus();
} else {
this._closeButton.focus();
}
}
private databaseSelected(dbName: string): void {
@@ -420,7 +461,6 @@ export class RestoreDialog extends Modal {
case ServiceOptionType.boolean:
propertyWidget = this.createCheckBoxHelper(container, option.description,
DialogHelper.getBooleanValueFromStringOrBoolean(option.defaultValue), () => this.onBooleanOptionChecked(optionName));
this._register(attachCheckboxStyler(propertyWidget, this._themeService));
break;
case ServiceOptionType.category:
propertyWidget = this.createSelectBoxHelper(container, option.description, option.categoryValues.map(c => c.displayName), DialogHelper.getCategoryDisplayName(option.categoryValues, option.defaultValue));
@@ -460,7 +500,11 @@ export class RestoreDialog extends Modal {
private createCheckBoxHelper(container: Builder, label: string, isChecked: boolean, onCheck: (viaKeyboard: boolean) => void): Checkbox {
let checkbox: Checkbox;
container.div({ class: 'dialog-input-section' }, (inputCellContainer) => {
checkbox = DialogHelper.createCheckBox(inputCellContainer, label, 'restore-checkbox', isChecked, onCheck);
checkbox = new Checkbox(inputCellContainer.getHTMLElement(), {
label: label,
checked: isChecked,
onChange: onCheck
});
});
return checkbox;
}
@@ -540,7 +584,7 @@ export class RestoreDialog extends Modal {
if (!isSame) {
this.viewModel.selectedBackupSets = selectedFiles;
this.validateRestore(false);
this.validateRestore(false, true);
}
}
@@ -561,11 +605,18 @@ export class RestoreDialog extends Modal {
this.onFilePathLoseFocus(params);
}));
this._register(DOM.addDisposableListener(this._browseFileButton.getElement(), DOM.EventType.CLICK, () => {
if (this._browseFileButton.enabled) {
this._browseFileButton.addListener(DOM.EventType.CLICK, () => {
this.onFileBrowserRequested();
});
this._browseFileButton.addListener(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
var event = new StandardKeyboardEvent(e);
if (event.keyCode === KeyCode.Enter) {
this.onFileBrowserRequested();
event.preventDefault();
event.stopPropagation();
}
}));
});
this._register(this._sourceDatabaseSelectBox.onDidSelect(selectedDatabase => {
this.onSourceDatabaseChanged(selectedDatabase.selected);
@@ -587,7 +638,7 @@ export class RestoreDialog extends Modal {
private onFileBrowsed(filepath: string) {
var oldFilePath = this._filePathInputBox.value;
if (DialogHelper.isNullOrWhiteSpace(this._filePathInputBox.value)) {
if (strings.isFalsyOrWhitespace(this._filePathInputBox.value)) {
this._filePathInputBox.value = filepath;
} else {
this._filePathInputBox.value = this._filePathInputBox.value + ', ' + filepath;
@@ -615,7 +666,6 @@ export class RestoreDialog extends Modal {
private onSourceDatabaseChanged(selectedDatabase: string) {
this.viewModel.sourceDatabaseName = selectedDatabase;
this.viewModel.selectedBackupSets = null;
this.viewModel.resetTailLogBackupFile();
this.validateRestore(true);
}
@@ -635,7 +685,8 @@ export class RestoreDialog extends Modal {
return this._restoreFromSelectBox.value === this._databaseTitle;
}
public validateRestore(overwriteTargetDatabase: boolean = false): void {
public validateRestore(overwriteTargetDatabase: boolean = false, isBackupFileCheckboxChanged: boolean = false): void {
this._isBackupFileCheckboxChanged = isBackupFileCheckboxChanged;
this.showSpinner();
this._restoreButton.enabled = false;
this._scriptButton.enabled = false;
@@ -679,6 +730,7 @@ export class RestoreDialog extends Modal {
this.onRestoreFromChanged(this._databaseTitle);
this._sourceDatabaseSelectBox.select(0);
this._panel.showTab(this._generalTabId);
this._isBackupFileCheckboxChanged = false;
this.removeErrorMessage();
this.resetRestoreContent();
}
@@ -688,7 +740,7 @@ export class RestoreDialog extends Modal {
this._ownerUri = ownerUri;
this.show();
this._filePathInputBox.focus();
this._restoreFromSelectBox.focus();
}
protected layout(height?: number): void {
@@ -771,45 +823,59 @@ export class RestoreDialog extends Modal {
}
this._fileListData.push(data);
// Select the first row for the table by default
this._fileListTable.setSelectedRows([0]);
this._fileListTable.setActiveCell(0, 0);
}
}
private updateBackupSetsToRestore(backupSetsToRestore: data.DatabaseFileInfo[]) {
this._restorePlanData.clear();
if (backupSetsToRestore && backupSetsToRestore.length > 0) {
if (!this._restorePlanColumn) {
let firstRow = backupSetsToRestore[0];
this._restorePlanColumn = firstRow.properties.map(item => {
return {
id: item.propertyName,
name: item.propertyDisplayName,
field: item.propertyName
};
});
let checkboxSelectColumn = new CheckboxSelectColumn({ title: this._restoreLabel });
this._register(attachCheckboxStyler(checkboxSelectColumn, this._themeService));
this._restorePlanColumn.unshift(checkboxSelectColumn.getColumnDefinition());
this._restorePlanTable.columns = this._restorePlanColumn;
this._restorePlanTable.registerPlugin(checkboxSelectColumn);
this._restorePlanTable.autosizeColumns();
}
let data = [];
if (this._isBackupFileCheckboxChanged) {
let selectedRow = [];
for (let i = 0; i < backupSetsToRestore.length; i++) {
let backupFile = backupSetsToRestore[i];
let newData = {};
for (let j = 0; j < backupFile.properties.length; j++) {
newData[backupFile.properties[j].propertyName] = backupFile.properties[j].propertyValueDisplayName;
}
data.push(newData);
if (backupFile.isSelected) {
if (backupSetsToRestore[i].isSelected) {
selectedRow.push(i);
}
}
this._restorePlanData.push(data);
this._restorePlanTable.setSelectedRows(selectedRow);
} else {
this._restorePlanData.clear();
if (backupSetsToRestore && backupSetsToRestore.length > 0) {
if (!this._restorePlanColumn) {
let firstRow = backupSetsToRestore[0];
this._restorePlanColumn = firstRow.properties.map(item => {
return {
id: item.propertyName,
name: item.propertyDisplayName,
field: item.propertyName
};
});
let checkboxSelectColumn = new CheckboxSelectColumn({ title: this._restoreLabel, toolTip: this._restoreLabel, width: 15 });
this._restorePlanColumn.unshift(checkboxSelectColumn.getColumnDefinition());
this._restorePlanTable.columns = this._restorePlanColumn;
this._restorePlanTable.registerPlugin(checkboxSelectColumn);
this._restorePlanTable.autosizeColumns();
}
let data = [];
let selectedRow = [];
for (let i = 0; i < backupSetsToRestore.length; i++) {
let backupFile = backupSetsToRestore[i];
let newData = {};
for (let j = 0; j < backupFile.properties.length; j++) {
newData[backupFile.properties[j].propertyName] = backupFile.properties[j].propertyValueDisplayName;
}
data.push(newData);
if (backupFile.isSelected) {
selectedRow.push(i);
}
}
this._restorePlanData.push(data);
this._restorePlanTable.setSelectedRows(selectedRow);
this._restorePlanTable.setActiveCell(selectedRow[0], 0);
}
}
}
}

View File

@@ -111,13 +111,10 @@ export class RestoreDialogController implements IRestoreDialogController {
let restoreDialog = this._restoreDialogs[providerId] as RestoreDialog;
restoreInfo.backupFilePaths = restoreDialog.viewModel.filePath;
// todo: Need to change restoreInfo.readHeaderFromMedia when implement restore from database
restoreInfo.readHeaderFromMedia = restoreDialog.viewModel.readHeaderFromMedia;
restoreInfo.selectedBackupSets = restoreDialog.viewModel.selectedBackupSets;
if (restoreDialog.viewModel.sourceDatabaseName) {
restoreInfo.sourceDatabaseName = restoreDialog.viewModel.sourceDatabaseName;
}
restoreInfo.sourceDatabaseName = restoreDialog.viewModel.sourceDatabaseName;
if (restoreDialog.viewModel.targetDatabaseName) {
restoreInfo.targetDatabaseName = restoreDialog.viewModel.targetDatabaseName;
}
@@ -221,7 +218,7 @@ export class RestoreDialogController implements IRestoreDialogController {
private fetchDatabases(provider: string): void {
this._connectionService.listDatabases(this._ownerUri).then(result => {
if (result && result.databaseNames) {
(<RestoreDialog> this._restoreDialogs[provider]).databaseListOptions = result.databaseNames;
(<RestoreDialog>this._restoreDialogs[provider]).databaseListOptions = result.databaseNames;
}
});
}

View File

@@ -82,8 +82,8 @@ export class RestoreViewModel {
}
/**
* Get option display value
*/
* Get option display value
*/
public getDisplayValue(optionMetadata: data.ServiceOption, optionValue: any): any {
let displayValue: any;
switch (optionMetadata.valueType) {
@@ -104,28 +104,28 @@ export class RestoreViewModel {
}
/**
* On restore from changed set readHeaderFromMedia and reset the source database names and selected database name based on isFromBackupFile value.
*/
* On restore from changed set readHeaderFromMedia and reset the source database names and selected database name based on isFromBackupFile value.
*/
public onRestoreFromChanged(isFromBackupFile: boolean) {
this.readHeaderFromMedia = isFromBackupFile;
if (isFromBackupFile) {
this.updateFilePath('');
this.updateSourceDatabaseNames([], '');
this.updateSourceDatabaseNames([], undefined);
} else {
this.updateSourceDatabaseNames(this.databaseList, this.databaseList[0]);
}
}
/**
* Get option metadata from the option map
*/
* Get option metadata from the option map
*/
public getOptionMetadata(optionName: string): data.ServiceOption {
return this._optionsMap[optionName] ? this._optionsMap[optionName].optionMetadata : undefined;
}
/**
* Set current value for restore option
*/
* Set current value for restore option
*/
public setOptionValue(optionName: string, value: any): void {
if (this._optionsMap[optionName]) {
this._optionsMap[optionName].currentValue = value;
@@ -133,19 +133,8 @@ export class RestoreViewModel {
}
/**
* Reset tail log backup file (temporary fix for bug#2838)
*/
public resetTailLogBackupFile(): void {
const tailLogBackupFileOption = 'tailLogBackupFile';
if (this._optionsMap[tailLogBackupFileOption]) {
this._optionsMap[tailLogBackupFileOption].currentValue = null;
this._optionsMap[tailLogBackupFileOption].defaultValue = null;
}
}
/**
* Get current value for restore option
*/
* Get current value for restore option
*/
public getOptionValue(optionName: string): any {
if (this._optionsMap[optionName]) {
return this._optionsMap[optionName].currentValue;
@@ -154,8 +143,8 @@ export class RestoreViewModel {
}
/**
* Get restore advanced options. Only return the options that are different from the default options
*/
* Get restore advanced options. Only return the options that are different from the default options
*/
public getRestoreAdvancedOptions(options: { [name: string]: any }) {
for (let key in this._optionsMap) {
let optionElement = this._optionsMap[key];
@@ -179,8 +168,8 @@ export class RestoreViewModel {
}
/**
* On restore plan response will update all the information from restore plan response
*/
* On restore plan response will update all the information from restore plan response
*/
public onRestorePlanResponse(restorePlanResponse: data.RestorePlanResponse): void {
if (restorePlanResponse.planDetails && restorePlanResponse.planDetails['lastBackupTaken']) {
this.updateLastBackupTaken(restorePlanResponse.planDetails['lastBackupTaken'].currentValue);
@@ -196,8 +185,8 @@ export class RestoreViewModel {
}
/**
* Update options with plan details
*/
* Update options with plan details
*/
public updateOptionWithPlanDetail(planDetails: { [key: string]: data.RestorePlanDetailInfo }): void {
if (planDetails) {
for (var key in planDetails) {
@@ -213,8 +202,8 @@ export class RestoreViewModel {
}
/**
* Update options with restore config info. The option values will be both default and current values.
*/
* Update options with restore config info. The option values will be both default and current values.
*/
public updateOptionWithConfigInfo(configInfo: { [key: string]: any }): void {
if (configInfo) {
if (configInfo['sourceDatabaseNamesWithBackupSets']) {
@@ -243,8 +232,8 @@ export class RestoreViewModel {
}
/**
* Update backup sets to restore
*/
* Update backup sets to restore
*/
public updateBackupSetsToRestore(backupSetsToRestore: data.DatabaseFileInfo[]): void {
this.selectedBackupSets = null;
if (backupSetsToRestore) {
@@ -259,11 +248,12 @@ export class RestoreViewModel {
}
/**
* Reset restore options to the default value
*/
* Reset restore options to the default value
*/
public resetRestoreOptions(databaseName: string): void {
this.sourceDatabaseName = databaseName ? databaseName : '';
this.updateTargetDatabaseName(databaseName);
this.updateSourceDatabaseNames([], databaseName);
this.updateSourceDatabaseNames([], this.sourceDatabaseName);
this.updateFilePath('');
this.updateLastBackupTaken('');
this.databaseList = [];
@@ -276,32 +266,32 @@ export class RestoreViewModel {
}
/**
* Update last backup taken
*/
* Update last backup taken
*/
public updateLastBackupTaken(value: string) {
this.lastBackupTaken = value;
this._onSetLastBackupTaken.fire(value);
}
/**
* Update file path
*/
* Update file path
*/
public updateFilePath(value: string) {
this.filePath = value;
this._onSetfilePath.fire(value);
}
/**
* Update source database names and selected database
*/
* Update source database names and selected database
*/
public updateSourceDatabaseNames(options: string[], selectedDatabase: string) {
this.sourceDatabaseName = selectedDatabase;
this._onSetSourceDatabaseNames.fire({ databaseNames: options, selectedDatabase: selectedDatabase });
}
/**
* Update target database name
*/
* Update target database name
*/
public updateTargetDatabaseName(value: string) {
this.targetDatabaseName = value;
this._onSetTargetDatabaseName.fire(value);

View File

@@ -62,7 +62,6 @@ export class EditDataEditor extends BaseEditor {
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
@IContextMenuService private _contextMenuService: IContextMenuService,
@IQueryModelService private _queryModelService: IQueryModelService,
@IEditorDescriptorService private _editorDescriptorService: IEditorDescriptorService,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IBootstrapService private _bootstrapService: IBootstrapService
) {

View File

@@ -6,17 +6,17 @@
'use strict';
import * as data from 'data';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { IConnectionManagementService, IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { FileBrowserTree } from 'sql/parts/fileBrowser/common/fileBrowserTree';
import { FileNode } from 'sql/parts/fileBrowser/common/fileNode';
import { FileBrowserDialog } from 'sql/parts/fileBrowser/fileBrowserDialog';
import { IFileBrowserService } from 'sql/parts/fileBrowser/common/interfaces';
import * as Constants from 'sql/common/constants';
import Event, { Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as strings from 'vs/base/common/strings';
export class FileBrowserService implements IFileBrowserService {
public _serviceBrand: any;
@@ -25,10 +25,10 @@ export class FileBrowserService implements IFileBrowserService {
private _onExpandFolder = new Emitter<FileNode>();
private _onPathValidate = new Emitter<data.FileBrowserValidatedParams>();
private _pathToFileNodeMap: { [path: string]: FileNode } = {};
private _expandResolveMap: { [key: string]: any } = {};
private _expandResolveMap: { [key: string]: any } = {};
static fileNodeId: number = 0;
constructor(@IConnectionManagementService private _connectionService: IConnectionManagementService,
constructor( @IConnectionManagementService private _connectionService: IConnectionManagementService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService) {
}
@@ -52,7 +52,7 @@ export class FileBrowserService implements IFileBrowserService {
public openFileBrowser(ownerUri: string, expandPath: string, fileFilters: string[], changeFilter: boolean): Thenable<boolean> {
return new Promise<boolean>((resolve, reject) => {
let provider = this.getProvider(ownerUri);
if (provider) {
if (provider) {
provider.openFileBrowser(ownerUri, expandPath, fileFilters, changeFilter).then(result => {
resolve(result);
}, error => {
@@ -71,11 +71,11 @@ export class FileBrowserService implements IFileBrowserService {
&& fileBrowserOpenedParams.fileTree.selectedNode
) {
var fileTree = this.convertFileTree(null, fileBrowserOpenedParams.fileTree.rootNode, fileBrowserOpenedParams.fileTree.selectedNode.fullPath, fileBrowserOpenedParams.ownerUri);
this._onAddFileTree.fire({rootNode: fileTree.rootNode, selectedNode: fileTree.selectedNode, expandedNodes: fileTree.expandedNodes});
this._onAddFileTree.fire({ rootNode: fileTree.rootNode, selectedNode: fileTree.selectedNode, expandedNodes: fileTree.expandedNodes });
} else {
let genericErrorMessage = localize('fileBrowserErrorMessage', 'An error occured while loading the file browser.');
let errorDialogTitle = localize('fileBrowserErrorDialogTitle', 'File Browser Error');
let errorMessage = DialogHelper.isNullOrWhiteSpace(fileBrowserOpenedParams.message) ? genericErrorMessage : fileBrowserOpenedParams.message;
let errorDialogTitle = localize('fileBrowserErrorDialogTitle', 'File browser error');
let errorMessage = strings.isFalsyOrWhitespace(fileBrowserOpenedParams.message) ? genericErrorMessage : fileBrowserOpenedParams.message;
this._errorMessageService.showDialog(Severity.Error, errorDialogTitle, errorMessage);
}
}
@@ -85,7 +85,7 @@ export class FileBrowserService implements IFileBrowserService {
let self = this;
return new Promise<FileNode[]>((resolve, reject) => {
let provider = this.getProvider(fileNode.ownerUri);
if (provider) {
if (provider) {
provider.expandFolderNode(fileNode.ownerUri, fileNode.fullPath).then(result => {
var mapKey = self.generateResolveMapKey(fileNode.ownerUri, fileNode.fullPath);
self._expandResolveMap[mapKey] = resolve;
@@ -102,12 +102,11 @@ export class FileBrowserService implements IFileBrowserService {
var mapKey = this.generateResolveMapKey(fileBrowserExpandedParams.ownerUri, fileBrowserExpandedParams.expandPath);
var expandResolve = this._expandResolveMap[mapKey];
if (expandResolve) {
if (fileBrowserExpandedParams.succeeded === true)
{
if (fileBrowserExpandedParams.succeeded === true) {
// get the expanded folder node
var expandedNode = this._pathToFileNodeMap[fileBrowserExpandedParams.expandPath];
if (expandedNode) {
if (fileBrowserExpandedParams.children && fileBrowserExpandedParams.children.length > 0) {
if (fileBrowserExpandedParams.children && fileBrowserExpandedParams.children.length > 0) {
expandedNode.children = this.convertChildren(expandedNode, fileBrowserExpandedParams.children, fileBrowserExpandedParams.ownerUri);
}
expandResolve(expandedNode.children ? expandedNode.children : []);
@@ -124,7 +123,7 @@ export class FileBrowserService implements IFileBrowserService {
public validateFilePaths(ownerUri: string, serviceType: string, selectedFiles: string[]): Thenable<boolean> {
return new Promise<boolean>((resolve, reject) => {
let provider = this.getProvider(ownerUri);
if (provider) {
if (provider) {
provider.validateFilePaths(ownerUri, serviceType, selectedFiles).then(result => {
resolve(result);
}, error => {

View File

@@ -7,11 +7,12 @@
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!./media/fileBrowserDialog';
import { Button } from 'sql/base/browser/ui/button/button';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { attachModalDialogStyler, attachButtonStyler } from 'sql/common/theme/styler';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { FileNode } from 'sql/parts/fileBrowser/common/fileNode';
import { FileBrowserTreeView } from 'sql/parts/fileBrowser/fileBrowserTreeView';
@@ -19,19 +20,20 @@ import { FileBrowserViewModel } from 'sql/parts/fileBrowser/fileBrowserViewModel
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Builder } from 'vs/base/browser/builder';
import { Button } from 'vs/base/browser/ui/button/button';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import Event, { Emitter } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { localize } from 'vs/nls';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { attachInputBoxStyler, attachButtonStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import * as DOM from 'vs/base/browser/dom';
import * as strings from 'vs/base/common/strings';
export class FileBrowserDialog extends Modal {
private _viewModel: FileBrowserViewModel;
@@ -77,9 +79,19 @@ export class FileBrowserDialog extends Modal {
if (this.backButton) {
this._register(DOM.addDisposableListener(this.backButton.getElement(), DOM.EventType.CLICK, () => {
this.backButton.addListener(DOM.EventType.CLICK, () => {
this.close();
}));
});
this.backButton.addListener(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
var event = new StandardKeyboardEvent(e);
if (event.keyCode === KeyCode.Enter) {
this.close();
event.preventDefault();
event.stopPropagation();
}
});
this._register(attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }));
}
@@ -141,7 +153,7 @@ export class FileBrowserDialog extends Modal {
}
private enableOkButton() {
if (DialogHelper.isNullOrWhiteSpace(this._selectedFilePath) || this._isFolderSelected === true) {
if (strings.isFalsyOrWhitespace(this._selectedFilePath) || this._isFolderSelected === true) {
this._okButton.enabled = false;
} else {
this._okButton.enabled = true;
@@ -175,7 +187,7 @@ export class FileBrowserDialog extends Modal {
}
private onFilePathBlur(param) {
if (!DialogHelper.isNullOrWhiteSpace(param.value)) {
if (!strings.isFalsyOrWhitespace(param.value)) {
this._viewModel.validateFilePaths([param.value]);
}
}
@@ -187,7 +199,7 @@ export class FileBrowserDialog extends Modal {
private handleOnValidate(succeeded: boolean, errorMessage: string) {
if (succeeded === false) {
if (DialogHelper.isNullOrWhiteSpace(errorMessage)) {
if (strings.isFalsyOrWhitespace(errorMessage)) {
errorMessage = 'The provided path is invalid.';
}
this._filePathInputBox.showMessage({ type: MessageType.ERROR, content: errorMessage });

View File

@@ -4,10 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as Utils from 'sql/parts/connection/common/utils';
import { FileNode } from 'sql/parts/fileBrowser/common/fileNode';
import { $ } from 'vs/base/browser/dom';
import dom = require('vs/base/browser/dom');
import { ITree, IRenderer } from 'vs/base/parts/tree/browser/tree';
import { FileKind } from 'vs/platform/files/common/files';
import URI from 'vs/base/common/uri';
@@ -55,7 +52,7 @@ export class FileBrowserRenderer implements IRenderer {
*/
public renderElement(tree: ITree, element: FileNode, templateId: string, templateData: IFileTemplateData): void {
if (element) {
templateData.label.element.style.display = 'block';
templateData.label.element.style.display = 'flex';
const extraClasses = ['explorer-item'];
var fileuri = URI.file(element.fullPath);

View File

@@ -40,57 +40,6 @@ export interface IGridIcon {
functionality: (batchId: number, resultId: number, index: number) => void;
}
/**
* Simplified interface for a Range object returned by the Rangy javascript plugin
*
* @export
* @interface IRange
*/
export interface IRange {
selectNodeContents(el): void;
/**
* Returns any user-visible text covered under the range, using standard HTML Range API calls
*
* @returns {string}
*
* @memberOf IRange
*/
toString(): string;
/**
* Replaces the current selection with this range. Equivalent to rangy.getSelection().setSingleRange(range).
*
*
* @memberOf IRange
*/
select(): void;
/**
* Returns the `Document` element containing the range
*
* @returns {Document}
*
* @memberOf IRange
*/
getDocument(): Document;
/**
* Detaches the range so it's no longer tracked by Rangy using DOM manipulation
*
*
* @memberOf IRange
*/
detach(): void;
/**
* Gets formatted text under a range. This is an improvement over toString() which contains unnecessary whitespac
*
* @returns {string}
*
* @memberOf IRange
*/
text(): string;
}
export interface IGridDataSet {
dataRows: IObservableCollection<IGridDataRow>;
columnDefinitions: IColumnDefinition[];

View File

@@ -51,6 +51,11 @@
padding-bottom: 1em;
}
#messageTable {
-webkit-user-select: text;
user-select: text;
}
.resultsMessageValue {
white-space: pre-line;
}

View File

@@ -48,9 +48,10 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
private complete = false;
private idMapping: { [row: number]: number } = {};
private currentCell: { row: number, column: number } = null;
private rowEditInProgress: boolean = false;
private removingNewRow: boolean = false;
// Current selected cell state
private currentCell: { row: number, column: number, isEditable: boolean };
private currentEditCellValue: string;
private removingNewRow: boolean;
// Edit Data functions
public onCellEditEnd: (event: { row: number, column: number, newValue: any }) => void;
@@ -71,7 +72,7 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
this._el.nativeElement.className = 'slickgridContainer';
let editDataParameters: EditDataComponentParams = this._bootstrapService.getBootstrapParams(this._el.nativeElement.tagName);
this.dataService = editDataParameters.dataService;
this.actionProvider = new EditDataGridActionProvider(this.dataService, this.onGridSelectAll(), this.onDeleteRow(), this.onRevertRow());
this.actionProvider = this._bootstrapService.instantiationService.createInstance(EditDataGridActionProvider, this.dataService, this.onGridSelectAll(), this.onDeleteRow(), this.onRevertRow());
}
/**
@@ -133,20 +134,8 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
};
this.onCellEditEnd = (event: { row: number, column: number, newValue: any }): void => {
self.rowEditInProgress = true;
// Update the cell accordingly
self.dataService.updateCell(this.idMapping[event.row], event.column, event.newValue)
.then(
result => {
self.setCellDirtyState(event.row, event.column + 1, result.cell.isDirty);
self.setRowDirtyState(event.row, result.isRowDirty);
},
error => {
// On error updating cell, jump back to the cell that was being edited
self.focusCell(event.row, event.column + 1);
}
);
// Store the value that was set
self.currentEditCellValue = event.newValue;
};
this.onCellEditBegin = (event: { row: number, column: number }): void => { };
@@ -199,11 +188,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
onDeleteRow(): (index: number) => void {
const self = this;
return (index: number): void => {
self.dataService.deleteRow(index).then(() => {
self.dataService.commitEdit().then(() => {
self.removeRow(index, 0);
});
});
self.dataService.deleteRow(index)
.then(() => self.dataService.commitEdit())
.then(() => self.removeRow(index, 0));
};
}
@@ -215,44 +202,87 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
// Perform a revert row operation
self.dataService.revertRow(index)
.then(() => { self.dataService.commitEdit(); })
.then(() => { self.refreshResultsets(); });
.then(() => self.dataService.commitEdit())
.then(() => self.refreshResultsets());
};
}
onCellSelect(row: number, column: number): void {
let self = this;
// TODO: We can skip this step if we're allowing multiple commits
if (this.rowEditInProgress) {
// We're in the middle of a row edit, so we need to commit if we move to a different row
if (row !== this.currentCell.row) {
this.dataService.commitEdit()
.then(
result => {
// Committing was successful. Clean the grid, turn off the row edit flag, then select again
self.setGridClean();
self.rowEditInProgress = false;
self.onCellSelect(row, column);
}, error => {
// Committing failed, so jump back to the last selected cell
self.focusCell(self.currentCell.row, self.currentCell.column);
}
);
return;
}
} else {
// We're not in the middle of a row edit, so we can move anywhere
// Checking for removing new row makes sure we don't re-add the new row after we've
// jumped to the first cell of the "new row"
if (this.isNullRow(row) && !this.removingNewRow) {
// We moved into the "new row", add another new row
this.addRow(row, column);
}
// Skip processing if the newly selected cell is undefined or we don't have column
// definition for the column (ie, the selection was reset)
if (row === undefined || column === undefined) {
return;
}
// Set the cell we moved to as the current cell
this.currentCell = { row: row, column: column };
// Skip processing if the cell hasn't moved (eg, we reset focus to the previous cell after a failed update)
if (this.currentCell.row === row && this.currentCell.column === column) {
return;
}
let cellSelectTasks: Promise<void> = Promise.resolve();
if (this.currentCell.isEditable && this.currentEditCellValue !== null && !this.removingNewRow) {
// We're exiting a read/write cell after having changed the value, update the cell value in the service
cellSelectTasks = cellSelectTasks.then(() => {
return self.dataService.updateCell(self.currentCell.row, self.currentCell.column - 1, self.currentEditCellValue)
.then(
result => {
// Cell update was successful, update the flags
self.currentEditCellValue = null;
self.setCellDirtyState(row, self.currentCell.column, result.cell.isDirty);
self.setRowDirtyState(row, result.isRowDirty);
return Promise.resolve();
},
error => {
// Cell update failed, jump back to the last cell we were on
self.focusCell(self.currentCell.row, self.currentCell.column, true);
return Promise.reject(null);
}
);
});
}
if (this.currentCell.row !== row) {
// We're changing row, commit the changes
cellSelectTasks = cellSelectTasks.then(() => {
return self.dataService.commitEdit()
.then(
result => {
// Committing was successful, clean the grid
self.setGridClean();
return Promise.resolve();
},
error => {
// Committing failed, jump back to the last selected cell
self.focusCell(self.currentCell.row, self.currentCell.column);
return Promise.reject(null);
}
);
});
}
if (this.isNullRow(row) && !this.removingNewRow) {
// We've entered the "new row", so we need to add a row and jump to it
cellSelectTasks = cellSelectTasks.then(() => {
self.addRow(row, column);
});
}
// At the end of a successful cell select, update the currently selected cell
cellSelectTasks = cellSelectTasks.then(() => {
self.currentCell = {
row: row,
column: column,
isEditable: self.dataSet.columnDefinitions[column - 1]
? self.dataSet.columnDefinitions[column - 1].isEditable
: false
};
});
// Cap off any failed promises, since they'll be handled
cellSelectTasks.catch(() => {});
}
handleComplete(self: EditDataComponent, event: any): void {
@@ -319,9 +349,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
self.messagesAdded = true;
self.onScroll(0);
// Reset selected cell state
this.currentCell = null;
this.rowEditInProgress = false;
// Setup the state of the selected cell
this.currentCell = { row: null, column: null, isEditable: null };
this.currentEditCellValue = null;
this.removingNewRow = false;
// HACK: unsafe reference to the slickgrid object
@@ -350,7 +380,6 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
self.placeHolderDataSets[i].resized.emit();
}
self._cd.detectChanges();
if (self.firstRender) {
@@ -380,7 +409,6 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
this.dataService.revertRow(this.idMapping[currentNewRowIndex])
.then(() => {
this.removeRow(currentNewRowIndex, 0);
this.rowEditInProgress = false;
});
handled = true;
}
@@ -434,7 +462,6 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
this.dataService.createRow();
// Adding an extra row for 'new row' functionality
this.rowEditInProgress = true;
this.dataSet.totalRows++;
this.dataSet.maxHeight = this.getMaxHeight(this.dataSet.totalRows);
this.dataSet.minHeight = this.getMinHeight(this.dataSet.totalRows);

View File

@@ -11,13 +11,18 @@ import { GridActionProvider } from 'sql/parts/grid/views/gridActions';
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, Action } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class EditDataGridActionProvider extends GridActionProvider {
constructor(dataService: DataService, selectAllCallback: (index: number) => void,
constructor(
dataService: DataService,
selectAllCallback: (index: number) => void,
private _deleteRowCallback: (index: number) => void,
private _revertRowCallback: (index: number) => void) {
super(dataService, selectAllCallback);
private _revertRowCallback: (index: number) => void,
@IInstantiationService instantiationService: IInstantiationService
) {
super(dataService, selectAllCallback, instantiationService);
}
/**
* Return actions given a click on an edit data grid

View File

@@ -5,13 +5,14 @@
'use strict';
import { IGridInfo, IRange, SaveFormat } from 'sql/parts/grid/common/interfaces';
import { IGridInfo, SaveFormat } from 'sql/parts/grid/common/interfaces';
import { DataService } from 'sql/parts/grid/services/dataService';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, Action } from 'vs/base/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export const GRID_SAVECSV_ID = 'grid.saveAsCsv';
export const GRID_SAVEJSON_ID = 'grid.saveAsJson';
@@ -26,7 +27,11 @@ export const TOGGLEMESSAGES_ID = 'grid.toggleMessagePane';
export class GridActionProvider {
constructor(protected _dataService: DataService, protected _selectAllCallback: (index: number) => void) {
constructor(
protected _dataService: DataService,
protected _selectAllCallback: (index: number) => void,
@IInstantiationService private _instantiationService: IInstantiationService
) {
}
@@ -50,7 +55,7 @@ export class GridActionProvider {
*/
public getMessagesActions(dataService: DataService, selectAllCallback: () => void): TPromise<IAction[]> {
let actions: IAction[] = [];
actions.push(new CopyMessagesAction(CopyMessagesAction.ID, CopyMessagesAction.LABEL));
actions.push(this._instantiationService.createInstance(CopyMessagesAction, CopyMessagesAction.ID, CopyMessagesAction.LABEL));
actions.push(new SelectAllMessagesAction(SelectAllMessagesAction.ID, SelectAllMessagesAction.LABEL, selectAllCallback));
return TPromise.as(actions);
}
@@ -150,14 +155,14 @@ export class CopyMessagesAction extends Action {
constructor(
id: string,
label: string
label: string,
@IClipboardService private clipboardService: IClipboardService
) {
super(id, label);
}
public run(selectedRange: IRange): TPromise<boolean> {
let selectedText = selectedRange.text();
WorkbenchUtils.executeCopy(selectedText);
public run(selectedRange: Selection): TPromise<boolean> {
this.clipboardService.writeText(selectedRange.toString());
return TPromise.as(true);
}
}

View File

@@ -17,7 +17,7 @@ import { IGridDataRow, ISlickRange, SlickGrid, FieldType } from 'angular2-slickg
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import * as Constants from 'sql/parts/query/common/constants';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import { IGridInfo, IRange, IGridDataSet, SaveFormat } from 'sql/parts/grid/common/interfaces';
import { IGridInfo, IGridDataSet, SaveFormat } from 'sql/parts/grid/common/interfaces';
import * as Utils from 'sql/parts/connection/common/utils';
import { DataService } from 'sql/parts/grid/services/dataService';
import * as actions from 'sql/parts/grid/views/gridActions';
@@ -25,8 +25,6 @@ import * as Services from 'sql/parts/grid/services/sharedServices';
import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
import { ResultsVisibleContext, ResultsGridFocussedContext, ResultsMessagesFocussedContext } from 'sql/parts/query/common/queryContext';
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import * as rangy from 'sql/base/node/rangy';
import { error } from 'sql/base/common/log';
import { IAction } from 'vs/base/common/actions';
@@ -228,7 +226,7 @@ export abstract class GridParentComponent {
private copySelection(): void {
let messageText = this.getMessageText();
if (messageText.length > 0) {
WorkbenchUtils.executeCopy(messageText);
this._bootstrapService.clipboardService.writeText(messageText);
} else {
let activeGrid = this.activeGrid;
let selection = this.slickgrids.toArray()[activeGrid].getSelectedRanges();
@@ -251,13 +249,17 @@ export abstract class GridParentComponent {
messageText = this.getMessageText();
}
if (messageText.length > 0) {
WorkbenchUtils.executeCopy(messageText);
this._bootstrapService.clipboardService.writeText(messageText);
}
}
private getMessageText(): string {
let range: IRange = this.getSelectedRangeUnderMessages();
return range ? range.text() : '';
if (document.activeElement === this.getMessagesElement()) {
if (window.getSelection()) {
return window.getSelection().toString();
}
}
return '';
}
private initShortcutsBase(): void {
@@ -337,10 +339,16 @@ export abstract class GridParentComponent {
}
openContextMenu(event, batchId, resultId, index): void {
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
let slick: any = this.slickgrids.toArray()[index];
let grid = slick._grid;
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
if (selection && selection.length === 0) {
let cell = (grid as Slick.Grid<any>).getCellFromEvent(event);
selection = [new Slick.Range(cell.row, cell.cell - 1)];
}
let rowIndex = grid.getCellFromEvent(event).row;
let actionContext: IGridInfo = {
@@ -356,8 +364,6 @@ export abstract class GridParentComponent {
getAnchor: () => anchor,
getActions: () => this.actionProvider.getGridActions(),
getKeyBinding: (action) => this._keybindingFor(action),
onHide: (wasCancelled?: boolean) => {
},
getActionsContext: () => (actionContext)
});
}
@@ -439,25 +445,12 @@ export abstract class GridParentComponent {
this._cd.detectChanges();
}
getSelectedRangeUnderMessages(): IRange {
let selectedRange: IRange = undefined;
let msgEl = this.getMessagesElement();
if (msgEl) {
selectedRange = this.getSelectedRangeWithin(msgEl);
getSelectedRangeUnderMessages(): Selection {
if (document.activeElement === this.getMessagesElement()) {
return window.getSelection();
} else {
return undefined;
}
return selectedRange;
}
getSelectedRangeWithin(el): IRange {
let selectedRange = undefined;
let sel = rangy.getSelection();
let elRange = <IRange>rangy.createRange();
elRange.selectNodeContents(el);
if (sel.rangeCount) {
selectedRange = sel.getRangeAt(0).intersection(elRange);
}
elRange.detach();
return selectedRange;
}
selectAllMessages(): void {
@@ -465,11 +458,12 @@ export abstract class GridParentComponent {
this.selectElementContents(msgEl);
}
selectElementContents(el): void {
let range = rangy.createRange();
selectElementContents(el: HTMLElement): void {
let range = document.createRange();
range.selectNodeContents(el);
let sel = rangy.getSelection();
sel.setSingleRange(range);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
/**

View File

@@ -14,7 +14,6 @@ import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
import { IGridDataSet } from 'sql/parts/grid/common/interfaces';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import { IInsightData, IInsightsView, IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
@@ -30,8 +29,7 @@ import {
} from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Builder } from 'vs/base/browser/builder';
import { attachSelectBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import Severity from 'vs/base/common/severity';
import URI from 'vs/base/common/uri';
import * as nls from 'vs/nls';
@@ -116,15 +114,17 @@ export class ChartViewerComponent implements OnInit, OnDestroy, IChartViewAction
// Init label first column checkbox
// Note: must use 'self' for callback
this.labelFirstColumnCheckBox = DialogHelper.createCheckBox(new Builder(this.labelFirstColumnElement.nativeElement),
this.labelFirstColumnLabel, 'chartView-checkbox', false, () => this.onLabelFirstColumnChanged());
this._disposables.push(attachCheckboxStyler(this.labelFirstColumnCheckBox, this._bootstrapService.themeService));
this.labelFirstColumnCheckBox = new Checkbox(this.labelFirstColumnElement.nativeElement, {
label: this.labelFirstColumnLabel,
onChange: () => this.onLabelFirstColumnChanged()
});
// Init label first column checkbox
// Note: must use 'self' for callback
this.columnsAsLabelsCheckBox = DialogHelper.createCheckBox(new Builder(this.columnsAsLabelsElement.nativeElement),
this.columnsAsLabelsLabel, 'chartView-checkbox', false, () => this.columnsAsLabelsChanged());
this._disposables.push(attachCheckboxStyler(this.columnsAsLabelsCheckBox, this._bootstrapService.themeService));
this.columnsAsLabelsCheckBox = new Checkbox(this.columnsAsLabelsElement.nativeElement, {
label: this.columnsAsLabelsLabel,
onChange: () => this.columnsAsLabelsChanged()
});
// Init legend dropdown
this.legendSelectBox = new SelectBox(this.legendOptions, this._chartConfig.legendPosition);
@@ -332,7 +332,7 @@ export class ChartViewerComponent implements OnInit, OnDestroy, IChartViewAction
this.componentHost.viewContainerRef.clear();
let componentRef = this.componentHost.viewContainerRef.createComponent(componentFactory);
this._chartComponent = <ChartInsight>componentRef.instance;
this._chartComponent.config = this._chartConfig;
this._chartComponent.setConfig(this._chartConfig);
this._chartComponent.data = this._executeResult;
this._chartComponent.options = mixin(this._chartComponent.options, { animation: { duration: 0 } });
if (this._chartComponent.init) {

View File

@@ -16,20 +16,17 @@ import {
ViewChildren, forwardRef, EventEmitter, Input, ViewChild
} from '@angular/core';
import { IGridDataRow, SlickGrid, VirtualizedCollection } from 'angular2-slickgrid';
import * as rangy from 'sql/base/node/rangy';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import * as Services from 'sql/parts/grid/services/sharedServices';
import { IGridIcon, IMessage, IRange, IGridDataSet } from 'sql/parts/grid/common/interfaces';
import { IGridIcon, IMessage, IGridDataSet } from 'sql/parts/grid/common/interfaces';
import { GridParentComponent } from 'sql/parts/grid/views/gridParentComponent';
import { GridActionProvider } from 'sql/parts/grid/views/gridActions';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import { QueryComponentParams } from 'sql/services/bootstrap/bootstrapParams';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { error } from 'sql/base/common/log';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import * as strings from 'vs/base/common/strings';
import { clone } from 'vs/base/common/objects';
import * as DOM from 'vs/base/browser/dom';
@@ -142,6 +139,7 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
private complete = false;
private sentPlans: Map<number, string> = new Map<number, string>();
private hasQueryPlan: boolean = false;
private queryPlanResultSetId: number = 0;
public queryExecutionStatus: EventEmitter<string> = new EventEmitter<string>();
public queryPlanAvailable: EventEmitter<string> = new EventEmitter<string>();
public showChartRequested: EventEmitter<IGridDataSet> = new EventEmitter<IGridDataSet>();
@@ -168,7 +166,7 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
const self = this;
this.dataService = this.queryParameters.dataService;
this.actionProvider = new GridActionProvider(this.dataService, this.onGridSelectAll());
this.actionProvider = this._bootstrapService.instantiationService.createInstance(GridActionProvider, this.dataService, this.onGridSelectAll());
this.baseInit();
this.setupResizeBind();
@@ -265,8 +263,8 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
}
// if this is a query plan resultset we haven't processed yet then forward to subscribers
if (self.hasQueryPlan && !self.sentPlans[resultSet.batchId]) {
self.sentPlans[resultSet.batchId] = rows.rows[0][0].displayValue;
if (self.hasQueryPlan && resultSet.id === self.queryPlanResultSetId && !self.sentPlans[resultSet.id]) {
self.sentPlans[resultSet.id] = rows.rows[0][0].displayValue;
self.queryPlanAvailable.emit(rows.rows[0][0].displayValue);
}
resolve(gridData);
@@ -322,6 +320,7 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
let column = resultSet.columnInfo[i];
if (column.columnName === 'Microsoft SQL Server 2005 XML Showplan') {
this.hasQueryPlan = true;
this.queryPlanResultSetId = resultSet.id;
break;
}
}
@@ -335,27 +334,10 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
self.onScroll(0);
}
/**
* Perform copy and do other actions for context menu on the messages component
*/
handleMessagesContextClick(event: { type: string, selectedRange: IRange }): void {
switch (event.type) {
case 'copySelection':
let selectedText = event.selectedRange.text();
WorkbenchUtils.executeCopy(selectedText);
break;
case 'selectall':
document.execCommand('selectAll');
break;
default:
break;
}
}
openMessagesContextMenu(event: any): void {
let self = this;
event.preventDefault();
let selectedRange: IRange = this.getSelectedRangeUnderMessages();
let selectedRange = this.getSelectedRangeUnderMessages();
let selectAllFunc = () => self.selectAllMessages();
let anchor = { x: event.x + 1, y: event.y };
this.contextMenuService.showContextMenu({
@@ -404,19 +386,6 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
}
self._cd.detectChanges();
if (self.firstRender) {
let setActive = () => {
if (self.firstRender && self.slickgrids.toArray().length > 0) {
self.slickgrids.toArray()[0].setActive();
self.firstRender = false;
}
};
setTimeout(() => {
setActive();
});
}
}, self.scrollTimeOutTime);
}
@@ -538,7 +507,7 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
// Deselect any text since we are navigating to a new grid
// Do this even if not switching grids, since this covers clicking on the grid after message selection
rangy.getSelection().removeAllRanges();
window.getSelection().removeAllRanges();
// check if you are actually trying to change navigation
if (this.activeGrid === targetIndex) {
@@ -600,4 +569,4 @@ export class QueryComponent extends GridParentComponent implements OnInit, OnDes
layout() {
this.resizeGrids();
}
}
}

View File

@@ -4,27 +4,27 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!sql/parts/insights/browser/media/insightsDialog';
import { Button } from 'sql/base/browser/ui/button/button';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { Modal } from 'sql/base/browser/ui/modal/modal';
import { IInsightsConfigDetails } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { attachModalDialogStyler, attachTableStyler } from 'sql/common/theme/styler';
import { attachButtonStyler, attachModalDialogStyler, attachTableStyler } from 'sql/common/theme/styler';
import { ITaskRegistry, Extensions as TaskExtensions } from 'sql/platform/tasks/taskRegistry';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { IInsightsDialogModel, ListResource, IInsightDialogActionContext } from 'sql/parts/insights/common/interfaces';
import { IInsightsDialogModel, ListResource, IInsightDialogActionContext, insertValueRegex } from 'sql/parts/insights/common/interfaces';
import { TableCollapsibleView } from 'sql/base/browser/ui/table/tableView';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
import { error } from 'sql/base/common/log';
import { Table } from 'sql/base/browser/ui/table/table';
import { CopyInsightDialogSelectionAction } from 'sql/parts/insights/common/insightDialogActions';
import { SplitView, ViewSizing } from 'sql/base/browser/ui/splitview/splitview';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import * as DOM from 'vs/base/browser/dom';
import { SplitView, ViewSizing } from 'vs/base/browser/ui/splitview/splitview';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IListService } from 'vs/platform/list/browser/listService';
import * as nls from 'vs/nls';
@@ -35,10 +35,8 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as types from 'vs/base/common/types';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { Button } from 'vs/base/browser/ui/button/button';
/* Regex that matches the form `${value}` */
export const insertValueRegex: RegExp = /\${(.*?)\}/;
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
const labelDisplay = nls.localize("item", "Item");
const valueDisplay = nls.localize("value", "Value");
@@ -81,6 +79,7 @@ export class InsightsDialogView extends Modal {
private _insight: IInsightsConfigDetails;
private _splitView: SplitView;
private _container: HTMLElement;
private _closeButton: Button;
private _topTable: Table<ListResource>;
private _topTableData: TableDataView<ListResource>;
private _bottomTable: Table<ListResource>;
@@ -213,12 +212,45 @@ export class InsightsDialogView extends Modal {
this._register(attachTableStyler(this._topTable, this._themeService));
this._register(attachTableStyler(this._bottomTable, this._themeService));
this._topTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
topTableView.focus();
e.stopImmediatePropagation();
} else if (event.equals(KeyCode.Tab)) {
bottomTableView.focus();
e.stopImmediatePropagation();
}
});
this._bottomTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
bottomTableView.focus();
e.stopImmediatePropagation();
} else if (event.equals(KeyCode.Tab)) {
let buttonFound = false;
for (let index = 0; index < this._taskButtonDisposables.length; index++) {
let element = this._taskButtonDisposables[index];
if (element instanceof Button && element.enabled) {
buttonFound = true;
element.focus();
break;
}
}
if (!buttonFound) {
this._closeButton.focus();
}
e.stopImmediatePropagation();
}
});
}
public render() {
super.render();
let button = this.addFooterButton('Close', () => this.close());
this._register(attachButtonStyler(button, this._themeService));
this._closeButton = this.addFooterButton('Close', () => this.close());
this._register(attachButtonStyler(this._closeButton, this._themeService));
this._register(attachModalDialogStyler(this, this._themeService));
}
@@ -274,6 +306,9 @@ export class InsightsDialogView extends Modal {
}
}
this.layout();
// Select and focus the top row
this._topTable.grid.gotoCell(0, 1);
}
public reset(): void {

View File

@@ -7,7 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IInsightsConfigDetails, IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { ITaskActionContext } from 'sql/workbench/common/actions';
import { BaseActionContext } from 'sql/workbench/common/actions';
export interface IInsightsDialogModel {
rows: string[][];
@@ -35,6 +35,9 @@ export interface IInsightsDialogService {
close();
}
export interface IInsightDialogActionContext extends ITaskActionContext {
export interface IInsightDialogActionContext extends BaseActionContext {
cellData: string;
}
/* Regex that matches the form `${value}` */
export const insertValueRegex: RegExp = /\${(.*?)\}/;

View File

@@ -6,11 +6,12 @@
import { IConnectionManagementService, IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IInsightsConfigDetails } from 'sql/parts/dashboard/widgets/insights/interfaces';
import QueryRunner from 'sql/parts/query/execution/queryRunner';
import QueryRunner, { EventType as QREvents } from 'sql/parts/query/execution/queryRunner';
import * as Utils from 'sql/parts/connection/common/utils';
import { IInsightsDialogModel } from 'sql/parts/insights/common/interfaces';
import { IInsightsDialogModel, insertValueRegex } from 'sql/parts/insights/common/interfaces';
import { error } from 'sql/base/common/log';
import { DbCellValue, IDbColumn, IResultMessage, QueryExecuteSubsetResult } from 'data';
import { DbCellValue, IDbColumn, QueryExecuteSubsetResult } from 'data';
import Severity from 'vs/base/common/severity';
import * as types from 'vs/base/common/types';
@@ -18,7 +19,7 @@ import * as pfs from 'vs/base/node/pfs';
import * as nls from 'vs/nls';
import { IMessageService } from 'vs/platform/message/common/message';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { error } from 'sql/base/common/log';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
export class InsightsDialogController {
private _queryRunner: QueryRunner;
@@ -33,6 +34,7 @@ export class InsightsDialogController {
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IWorkspaceContextService private _workspaceContextService: IWorkspaceContextService
) { }
public update(input: IInsightsConfigDetails, connectionProfile: IConnectionProfile): Thenable<void> {
@@ -44,18 +46,45 @@ export class InsightsDialogController {
}
if (types.isStringArray(input.query)) {
return this.createQuery(input.query.join(' '), connectionProfile).catch(e => {
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights Error"), e);
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), e);
}).then(() => undefined);
} else if (types.isString(input.query)) {
return this.createQuery(input.query, connectionProfile).catch(e => {
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights Error"), e);
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), e);
}).then(() => undefined);
} else if (types.isString(input.queryFile)) {
let filePath = input.queryFile;
// check for workspace relative path
let match = filePath.match(insertValueRegex);
if (match && match.length > 0 && match[1] === 'workspaceRoot') {
filePath = filePath.replace(match[0], '');
switch (this._workspaceContextService.getWorkbenchState()) {
case WorkbenchState.FOLDER:
filePath = this._workspaceContextService.getWorkspace().folders[0].toResource(filePath).fsPath;
break;
case WorkbenchState.WORKSPACE:
let filePathArray = filePath.split('/');
// filter out empty sections
filePathArray = filePathArray.filter(i => !!i);
let folder = this._workspaceContextService.getWorkspace().folders.find(i => i.name === filePathArray[0]);
if (!folder) {
return Promise.reject<void>(new Error(`Could not find workspace folder ${filePathArray[0]}`));
}
// remove the folder name from the filepath
filePathArray.shift();
// rejoin the filepath after doing the work to find the right folder
filePath = '/' + filePathArray.join('/');
filePath = folder.toResource(filePath).fsPath;
break;
}
}
return new Promise((resolve, reject) => {
pfs.readFile(input.queryFile).then(
pfs.readFile(filePath).then(
buffer => {
this.createQuery(buffer.toString(), connectionProfile).catch(e => {
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights Error"), e);
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), e);
}).then(() => resolve());
},
error => {
@@ -115,14 +144,14 @@ export class InsightsDialogController {
}
private addQueryEventListeners(queryRunner: QueryRunner): void {
queryRunner.eventEmitter.on('complete', () => {
queryRunner.addListener(QREvents.COMPLETE, () => {
this.queryComplete().catch(error => {
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights Error"), error);
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), error);
});
});
queryRunner.eventEmitter.on('message', (message: IResultMessage) => {
queryRunner.addListener(QREvents.MESSAGE, message => {
if (message.isError) {
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights Error"), message.message);
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), message.message);
}
});
}

View File

@@ -4,10 +4,9 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import * as nls from 'vs/nls';
@@ -17,9 +16,8 @@ import { ProfilerEditor } from 'sql/parts/profiler/editor/profilerEditor';
import { PROFILER_SESSION_TEMPLATE_SETTINGS, IProfilerSessionTemplate } from 'sql/parts/profiler/service/interfaces';
const profilerDescriptor = new EditorDescriptor(
ProfilerEditor,
ProfilerEditor.ID,
'Profiler',
'sql/parts/profiler/editor/profilerEditor',
'ProfilerEditor'
);

View File

@@ -10,7 +10,7 @@ import { registerTask } from 'sql/platform/tasks/taskRegistry';
import { NewProfilerAction } from './profilerActions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import * as nls from 'vs/nls';
import { IJSONSchema } from 'vs/base/common/jsonSchema';

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