mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-19 11:31:40 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 + ')';
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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', " Help configuring Kerberos is available at ") + helpLink,
|
||||
localize('kerberosKinit', " 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
10
src/sql/parts/dashboard/common/dashboardPage.css
Normal file
10
src/sql/parts/dashboard/common/dashboardPage.css
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
12
src/sql/parts/dashboard/dashboard.css
Normal file
12
src/sql/parts/dashboard/dashboard.css
Normal 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;
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
@@ -76,7 +76,7 @@ export class DashboardInput extends EditorInput {
|
||||
public getResource(): URI {
|
||||
return URI.from({
|
||||
scheme: 'dashboard',
|
||||
path: '.dashboard'
|
||||
path: 'dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
392
src/sql/parts/dashboard/widgets/explorer/explorerTree.ts
Normal file
392
src/sql/parts/dashboard/widgets/explorer/explorerTree.ts
Normal 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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ explorer-widget .list-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
margin-left: -33px;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface IInsightData {
|
||||
|
||||
export interface IInsightsView {
|
||||
data: IInsightData;
|
||||
config?: { [key: string]: any };
|
||||
setConfig?: (config: { [key: string]: any }) => void;
|
||||
init?: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(' ')));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,4 @@ let imageInsightSchema: IJSONSchema = {
|
||||
}
|
||||
};
|
||||
|
||||
registerInsight('image', '', imageInsightSchema, ImageInsight);
|
||||
registerInsight('image', '', imageInsightSchema, ImageInsight);
|
||||
|
||||
@@ -104,5 +104,5 @@ export const properties: Array<ProviderProperties> = [
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
];
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
src/sql/parts/dashboard/widgets/tasks/media/taskWidget.css
Normal file
36
src/sql/parts/dashboard/widgets/tasks/media/taskWidget.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
#messageTable {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.resultsMessageValue {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = /\${(.*?)\}/;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user