mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-16 03:21:39 -04:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
230
src/sql/parts/accountManagement/accountDialog/accountDialog.ts
Normal file
230
src/sql/parts/accountManagement/accountDialog/accountDialog.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/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 { 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 { ActionRunner } from 'vs/base/common/actions';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
|
||||
import * as data from 'data';
|
||||
import { Modal } from 'sql/base/browser/ui/modal/modal';
|
||||
import { attachModalDialogStyler } 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';
|
||||
import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/services/accountManagement/eventTypes';
|
||||
import { FixedListView } from 'sql/platform/views/fixedListView';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
export class AccountDialog extends Modal {
|
||||
public static ACCOUNTLIST_HEIGHT = 77;
|
||||
|
||||
public viewModel: AccountViewModel;
|
||||
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////
|
||||
private _providerViews: { [providerId: string]: FixedListView<data.Account> } = {};
|
||||
|
||||
private _closeButton: Button;
|
||||
private _delegate: AccountListDelegate;
|
||||
private _accountRenderer: AccountListRenderer;
|
||||
private _actionRunner: ActionRunner;
|
||||
private _splitView: SplitView;
|
||||
private _container: HTMLElement;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _onAddAccountErrorEmitter: Emitter<string>;
|
||||
public get onAddAccountErrorEvent(): Event<string> { return this._onAddAccountErrorEmitter.event; }
|
||||
|
||||
private _onCloseEmitter: Emitter<void>;
|
||||
public get onCloseEvent(): Event<void> { return this._onCloseEmitter.event; }
|
||||
|
||||
constructor(
|
||||
@IPartService partService: IPartService,
|
||||
@IThemeService private _themeService: IThemeService,
|
||||
@IListService private _listService: IListService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService,
|
||||
@IKeybindingService private _keybindingService: IKeybindingService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(
|
||||
localize('linkedAccounts', 'Linked Accounts'),
|
||||
TelemetryKeys.Accounts,
|
||||
partService,
|
||||
telemetryService,
|
||||
contextKeyService,
|
||||
{ hasSpinner: true }
|
||||
);
|
||||
let self = this;
|
||||
|
||||
this._delegate = new AccountListDelegate(AccountDialog.ACCOUNTLIST_HEIGHT);
|
||||
this._accountRenderer = this._instantiationService.createInstance(AccountListRenderer);
|
||||
this._actionRunner = new ActionRunner();
|
||||
|
||||
// Setup the event emitters
|
||||
this._onAddAccountErrorEmitter = new Emitter<string>();
|
||||
this._onCloseEmitter = new Emitter<void>();
|
||||
|
||||
// Create the view model and wire up the events
|
||||
this.viewModel = this._instantiationService.createInstance(AccountViewModel);
|
||||
this.viewModel.addProviderEvent(arg => { self.addProvider(arg); });
|
||||
this.viewModel.removeProviderEvent(arg => { self.removeProvider(arg); });
|
||||
this.viewModel.updateAccountListEvent(arg => { self.updateProviderAccounts(arg); });
|
||||
}
|
||||
|
||||
// MODAL OVERRIDE METHODS //////////////////////////////////////////////
|
||||
protected layout(height?: number): void {
|
||||
// Ignore height as it's a subcomponent being laid out
|
||||
this._splitView.layout(DOM.getContentHeight(this._container));
|
||||
}
|
||||
|
||||
public render() {
|
||||
let self = this;
|
||||
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
this._closeButton = this.addFooterButton(localize('close', 'Close'), () => this.close());
|
||||
this.registerListeners();
|
||||
|
||||
// Load the initial contents of the view model
|
||||
this.viewModel.initialize()
|
||||
.then(addedProviders => {
|
||||
for (let addedProvider of addedProviders) {
|
||||
self.addProvider(addedProvider);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
this._container = container;
|
||||
let viewBody = DOM.$('div.account-view');
|
||||
DOM.append(container, viewBody);
|
||||
this._splitView = new SplitView(viewBody);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(attachButtonStyler(this._closeButton, this._themeService));
|
||||
}
|
||||
|
||||
/* Overwrite escape key behavior */
|
||||
protected onClose() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this._onCloseEmitter.fire();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
for (let key in this._providerViews) {
|
||||
this._providerViews[key].dispose();
|
||||
delete this._providerViews[key];
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private addProvider(newProvider: AccountProviderAddedEventParams) {
|
||||
let self = this;
|
||||
|
||||
// Skip adding the provider if it already exists
|
||||
if (this._providerViews[newProvider.addedProvider.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Account provider doesn't exist, so add it
|
||||
// Create a scoped add account action
|
||||
let addAccountAction = this._instantiationService.createInstance(
|
||||
AddAccountAction,
|
||||
newProvider.addedProvider.id
|
||||
);
|
||||
addAccountAction.addAccountCompleteEvent(() => { self.hideSpinner(); });
|
||||
addAccountAction.addAccountErrorEvent(msg => { self._onAddAccountErrorEmitter.fire(msg); });
|
||||
addAccountAction.addAccountStartEvent(() => { self.showSpinner(); });
|
||||
|
||||
// Create a fixed list view for the account provider
|
||||
let providerViewContainer = DOM.$('.provider-view');
|
||||
let accountList = new List<data.Account>(providerViewContainer, this._delegate, [this._accountRenderer]);
|
||||
let providerView = new FixedListView<data.Account>(
|
||||
undefined,
|
||||
false,
|
||||
newProvider.addedProvider.displayName,
|
||||
accountList,
|
||||
providerViewContainer,
|
||||
22,
|
||||
[addAccountAction],
|
||||
this._actionRunner,
|
||||
this._contextMenuService,
|
||||
this._keybindingService,
|
||||
this._themeService
|
||||
);
|
||||
|
||||
// Append the list view to the split view
|
||||
this._splitView.addView(providerView);
|
||||
this._register(attachListStyler(accountList, this._themeService));
|
||||
this._register(this._listService.register(accountList));
|
||||
this._splitView.layout(DOM.getContentHeight(this._container));
|
||||
|
||||
// Set the initial items of the list
|
||||
providerView.updateList(newProvider.initialAccounts);
|
||||
this.layout();
|
||||
|
||||
// Store the view for the provider
|
||||
this._providerViews[newProvider.addedProvider.id] = providerView;
|
||||
}
|
||||
|
||||
private removeProvider(removedProvider: data.AccountProviderMetadata) {
|
||||
// Skip removing the provider if it doesn't exist
|
||||
let providerView = this._providerViews[removedProvider.id];
|
||||
if (!providerView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the list view from the split view
|
||||
this._splitView.removeView(providerView);
|
||||
this._splitView.layout(DOM.getContentHeight(this._container));
|
||||
|
||||
// Remove the list view from our internal map
|
||||
delete this._providerViews[removedProvider.id];
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private updateProviderAccounts(args: UpdateAccountListEventParams) {
|
||||
let providerMapping = this._providerViews[args.providerId];
|
||||
if (!providerMapping) {
|
||||
return;
|
||||
}
|
||||
providerMapping.updateList(args.accountList);
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { AccountDialog } from 'sql/parts/accountManagement/accountDialog/accountDialog';
|
||||
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class AccountDialogController {
|
||||
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////
|
||||
private _addAccountErrorTitle = localize('addAccountErrorTitle', 'Error adding account');
|
||||
|
||||
private _accountDialog: AccountDialog;
|
||||
public get accountDialog(): AccountDialog { return this._accountDialog; }
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Open account dialog
|
||||
*/
|
||||
public openAccountDialog(): void {
|
||||
let self = this;
|
||||
|
||||
// Create a new dialog if one doesn't exist
|
||||
if (!this._accountDialog) {
|
||||
this._accountDialog = this._instantiationService.createInstance(AccountDialog);
|
||||
this._accountDialog.onAddAccountErrorEvent(msg => { self.handleOnAddAccountError(msg); });
|
||||
this._accountDialog.onCloseEvent(() => { self.handleOnClose(); });
|
||||
this._accountDialog.render();
|
||||
}
|
||||
|
||||
// Open the dialog
|
||||
this._accountDialog.open();
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private handleOnClose(): void { }
|
||||
|
||||
private handleOnAddAccountError(msg: string): void {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._addAccountErrorTitle, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as data from 'data';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
|
||||
import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/services/accountManagement/eventTypes';
|
||||
|
||||
/**
|
||||
* View model for account dialog
|
||||
*/
|
||||
export class AccountViewModel {
|
||||
// EVENTING ///////////////////////////////////////////////////////
|
||||
private _addProviderEmitter: Emitter<AccountProviderAddedEventParams>;
|
||||
public get addProviderEvent(): Event<AccountProviderAddedEventParams> { return this._addProviderEmitter.event; }
|
||||
|
||||
private _removeProviderEmitter: Emitter<data.AccountProviderMetadata>;
|
||||
public get removeProviderEvent(): Event<data.AccountProviderMetadata> { return this._removeProviderEmitter.event; }
|
||||
|
||||
private _updateAccountListEmitter: Emitter<UpdateAccountListEventParams>;
|
||||
public get updateAccountListEvent(): Event<UpdateAccountListEventParams> { return this._updateAccountListEmitter.event; }
|
||||
|
||||
constructor(@IAccountManagementService private _accountManagementService: IAccountManagementService) {
|
||||
let self = this;
|
||||
|
||||
// Create our event emitters
|
||||
this._addProviderEmitter = new Emitter<AccountProviderAddedEventParams>();
|
||||
this._removeProviderEmitter = new Emitter<data.AccountProviderMetadata>();
|
||||
this._updateAccountListEmitter = new Emitter<UpdateAccountListEventParams>();
|
||||
|
||||
// Register handlers for any changes to the providers or accounts
|
||||
this._accountManagementService.addAccountProviderEvent(arg => self._addProviderEmitter.fire(arg));
|
||||
this._accountManagementService.removeAccountProviderEvent(arg => self._removeProviderEmitter.fire(arg));
|
||||
this._accountManagementService.updateAccountListEvent(arg => self._updateAccountListEmitter.fire(arg));
|
||||
}
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
/**
|
||||
* Loads an initial list of account providers and accounts from the account management service
|
||||
* and fires an event after each provider/accounts has been loaded.
|
||||
*
|
||||
*/
|
||||
public initialize(): Thenable<AccountProviderAddedEventParams[]> {
|
||||
let self = this;
|
||||
|
||||
// Load a baseline of the account provider metadata and accounts
|
||||
// 1) Get all the providers from the account management service
|
||||
// 2) For each provider, get the accounts
|
||||
// 3) Build parameters to add a provider and return it
|
||||
return this._accountManagementService.getAccountProviderMetadata()
|
||||
.then(
|
||||
(providers: data.AccountProviderMetadata[]) => {
|
||||
let promises = providers.map(provider => {
|
||||
return self._accountManagementService.getAccountsForProvider(provider.id)
|
||||
.then(
|
||||
accounts => <AccountProviderAddedEventParams> {
|
||||
addedProvider: provider,
|
||||
initialAccounts: accounts
|
||||
},
|
||||
() => { /* Swallow failures at getting accounts, we'll just hide that provider */ });
|
||||
});
|
||||
return Promise.all(promises);
|
||||
},
|
||||
() => {
|
||||
/* Swallow failures and just pretend we don't have any providers */
|
||||
return [];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.account-view .monaco-split-view .split-view-view .header {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.account-view .provider-view .list-row {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.account-view .provider-view .list-row .icon {
|
||||
flex: 0 0 50px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-size: 50px;
|
||||
}
|
||||
|
||||
.account-view .provider-view .list-row .icon .badge {
|
||||
position: absolute;
|
||||
top: 43px;
|
||||
left: 43px;
|
||||
overflow: hidden;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.account-view .provider-view .list-row .icon .badge .badge-content {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-size: 22px;
|
||||
}
|
||||
|
||||
.account-view .provider-view .list-row .actions-container {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.account-view .provider-view .list-row .actions-container .action-item .action-label {
|
||||
width: 16px;
|
||||
margin-left: 20px;
|
||||
margin-right: 10px;
|
||||
background-size: 16px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.account-view .provider-view .list-row .actions-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.account-view .provider-view .monaco-list .monaco-list-row:hover .list-row .actions-container,
|
||||
.account-view .provider-view .monaco-list .monaco-list-row.selected .list-row .actions-container,
|
||||
.account-view .provider-view .monaco-list .monaco-list-row.focused .list-row .actions-container{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.account-view .split-view-view .header .title {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.account-view .split-view-view .header .actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.account-view .split-view-view .header .actions .action-item .action-label {
|
||||
width: 30px;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
margin-right: 0;
|
||||
height: 22px;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/accountListStatusbarItem';
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { $, append } from 'vs/base/browser/dom';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { localize } from 'vs/nls';
|
||||
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;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService
|
||||
) {
|
||||
this._toDispose = [];
|
||||
}
|
||||
|
||||
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'));
|
||||
|
||||
return combinedDisposable(this._toDispose);
|
||||
}
|
||||
|
||||
private _onClick() {
|
||||
if (!this._manageLinkedAccountAction) {
|
||||
this._manageLinkedAccountAction = this._instantiationService.createInstance(ManageLinkedAccountAction, ManageLinkedAccountAction.ID, ManageLinkedAccountAction.LABEL);
|
||||
}
|
||||
this._manageLinkedAccountAction.run().done(null, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
|
||||
export class ManageLinkedAccountAction extends Action {
|
||||
public static ID = 'sql.action.accounts.manageLinkedAccount';
|
||||
public static LABEL = localize('manageLinedAccounts', 'Manage Linked Accounts');
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IAccountManagementService protected _accountManagementService: IAccountManagementService) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): TPromise<any> {
|
||||
return new TPromise<any>(() => this._accountManagementService.openAccountListDialog());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.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;
|
||||
width: 12px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 10.89"><defs><style>.cls-1{fill:#fff;}</style></defs><title>accounts_statusbar_inverse_11x11</title><path class="cls-1" d="M10.08,5.66a3.44,3.44,0,0,0-.34-.75,3.47,3.47,0,0,0-.52-.64A3.23,3.23,0,0,0,8.8,4a2.28,2.28,0,0,0,.7-1.65,2.27,2.27,0,0,0-.18-.89A2.31,2.31,0,0,0,8.09.19,2.19,2.19,0,0,0,7.19,0,2.16,2.16,0,0,0,6.3.19a2.41,2.41,0,0,0-.73.49,2.41,2.41,0,0,0-.49.73,2.16,2.16,0,0,0-.19.89,2.22,2.22,0,0,0,.25,1A2.31,2.31,0,0,0,5.58,4a3.62,3.62,0,0,0-.52.41,3.62,3.62,0,0,0-.41.52A2.31,2.31,0,0,0,4,4.44a2.26,2.26,0,0,0-1-.25,2.16,2.16,0,0,0-.89.19,2.41,2.41,0,0,0-.73.49,2.41,2.41,0,0,0-.49.73,2.16,2.16,0,0,0-.19.89,2.22,2.22,0,0,0,.25,1,2.31,2.31,0,0,0,.45.61A3.7,3.7,0,0,0,1,8.47a3.47,3.47,0,0,0-.52.64,2.88,2.88,0,0,0-.34.75,3.06,3.06,0,0,0-.12.83v.21H1.12v-.21A1.83,1.83,0,0,1,1.27,10a1.9,1.9,0,0,1,1-1A1.83,1.83,0,0,1,3,8.8a1.86,1.86,0,0,1,.73.15,2,2,0,0,1,.6.4,1.9,1.9,0,0,1,.4.6,1.85,1.85,0,0,1,.15.73v.21H6v-.21a3.05,3.05,0,0,0-.11-.83,3.44,3.44,0,0,0-.34-.75A3.25,3.25,0,0,0,5,8.47a3.23,3.23,0,0,0-.43-.32,2.28,2.28,0,0,0,.7-1.65,1.83,1.83,0,0,1,.15-.73,1.9,1.9,0,0,1,1-1,1.83,1.83,0,0,1,.73-.15,1.86,1.86,0,0,1,.73.15,1.9,1.9,0,0,1,1,1,1.82,1.82,0,0,1,.15.73V6.7h1.12V6.49A3.05,3.05,0,0,0,10.08,5.66ZM4.1,7a1.24,1.24,0,0,1-.64.64A1.13,1.13,0,0,1,3,7.68a1.1,1.1,0,0,1-.45-.09,1.21,1.21,0,0,1-.38-.26A1.1,1.1,0,0,1,1.91,7a1.11,1.11,0,0,1-.1-.46A1.13,1.13,0,0,1,1.91,6a1.1,1.1,0,0,1,.26-.38,1.1,1.1,0,0,1,.38-.26A1.1,1.1,0,0,1,3,5.31a1.16,1.16,0,0,1,.46.1A1.24,1.24,0,0,1,4.1,6a1.15,1.15,0,0,1,.09.45A1.13,1.13,0,0,1,4.1,7ZM8.29,2.76a1.24,1.24,0,0,1-.64.64,1.13,1.13,0,0,1-.46.09,1.1,1.1,0,0,1-.45-.09,1.21,1.21,0,0,1-.38-.26,1.1,1.1,0,0,1-.26-.38A1.14,1.14,0,0,1,6,2.3a1.15,1.15,0,0,1,.1-.45,1.1,1.1,0,0,1,.26-.38,1.19,1.19,0,0,1,.83-.36,1.16,1.16,0,0,1,.46.1A1.1,1.1,0,0,1,8,1.47a1.21,1.21,0,0,1,.26.38,1.15,1.15,0,0,1,.09.45A1.13,1.13,0,0,1,8.29,2.76Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
232
src/sql/parts/accountManagement/accountPicker/accountPicker.ts
Normal file
232
src/sql/parts/accountManagement/accountPicker/accountPicker.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/accountPicker';
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IDropdownOptions } from 'vs/base/browser/ui/dropdown/dropdown';
|
||||
import { IListEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { buttonBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
|
||||
import * as data from 'data';
|
||||
import { DropdownList } from 'sql/base/browser/ui/dropdownList/dropdownList';
|
||||
import { attachDropdownStyler } from 'sql/common/theme/styler';
|
||||
import { AddAccountAction, RefreshAccountAction } from 'sql/parts/accountManagement/common/accountActions';
|
||||
import { AccountPickerListRenderer, AccountListDelegate } from 'sql/parts/accountManagement/common/accountListRenderer';
|
||||
import { AccountPickerViewModel } from 'sql/parts/accountManagement/accountPicker/accountPickerViewModel';
|
||||
|
||||
export class AccountPicker extends Disposable {
|
||||
public static ACCOUNTPICKERLIST_HEIGHT = 47;
|
||||
public viewModel: AccountPickerViewModel;
|
||||
private _accountList: List<data.Account>;
|
||||
private _rootElement: HTMLElement;
|
||||
private _refreshContainer: HTMLElement;
|
||||
private _listContainer: HTMLElement;
|
||||
private _dropdown: DropdownList;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _addAccountCompleteEmitter: Emitter<void>;
|
||||
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
|
||||
|
||||
private _addAccountErrorEmitter: Emitter<string>;
|
||||
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
|
||||
|
||||
private _addAccountStartEmitter: Emitter<void>;
|
||||
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
|
||||
|
||||
private _onAccountSelectionChangeEvent: Emitter<data.Account>;
|
||||
public get onAccountSelectionChangeEvent(): Event<data.Account> { return this._onAccountSelectionChangeEvent.event; }
|
||||
|
||||
constructor(
|
||||
private _providerId: string,
|
||||
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IContextViewService private _contextViewService: IContextViewService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Create event emitters
|
||||
this._addAccountCompleteEmitter = new Emitter<void>();
|
||||
this._addAccountErrorEmitter = new Emitter<string>();
|
||||
this._addAccountStartEmitter = new Emitter<void>();
|
||||
this._onAccountSelectionChangeEvent = new Emitter<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 => {
|
||||
if (arg.providerId === this._providerId) {
|
||||
this.updateAccountList(arg.accountList);
|
||||
}
|
||||
});
|
||||
|
||||
this.createAccountPickerComponent();
|
||||
}
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
/**
|
||||
* Render account picker
|
||||
*/
|
||||
public render(container: HTMLElement): void {
|
||||
DOM.append(container, this._rootElement);
|
||||
}
|
||||
|
||||
private createAccountPickerComponent() {
|
||||
this._rootElement = DOM.$('div.account-picker-container');
|
||||
|
||||
// Create a dropdown for account picker
|
||||
let option: IDropdownOptions = {
|
||||
contextViewProvider: this._contextViewService,
|
||||
labelRenderer: (container) => this.renderLabel(container)
|
||||
};
|
||||
|
||||
// Create the add account action
|
||||
let addAccountAction = this._instantiationService.createInstance(AddAccountAction, this._providerId);
|
||||
addAccountAction.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());
|
||||
addAccountAction.addAccountErrorEvent((msg) => this._addAccountErrorEmitter.fire(msg));
|
||||
addAccountAction.addAccountStartEvent(() => this._addAccountStartEmitter.fire());
|
||||
|
||||
this._dropdown = this._register(new DropdownList(this._rootElement, option, this._listContainer, this._accountList, this._themeService, addAccountAction));
|
||||
this._register(attachDropdownStyler(this._dropdown, this._themeService));
|
||||
this._register(this._accountList.onSelectionChange((e: IListEvent<data.Account>) => {
|
||||
if (e.elements.length === 1) {
|
||||
this._dropdown.renderLabel();
|
||||
this.onAccountSelectionChange(e.elements[0]);
|
||||
}
|
||||
}));
|
||||
|
||||
// Create refresh account action
|
||||
this._refreshContainer = DOM.append(this._rootElement, DOM.$('div.refresh-container'));
|
||||
DOM.append(this._refreshContainer, DOM.$('div.icon warning'));
|
||||
let actionBar = new ActionBar(this._refreshContainer, { animated: false });
|
||||
actionBar.push(new RefreshAccountAction(RefreshAccountAction.ID, RefreshAccountAction.LABEL), { icon: false, label: true });
|
||||
|
||||
if (this._accountList.length > 0) {
|
||||
this._accountList.setSelection([0]);
|
||||
this.onAccountSelectionChange(this._accountList.getSelectedElements()[0]);
|
||||
} else {
|
||||
new Builder(this._refreshContainer).hide();
|
||||
}
|
||||
|
||||
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
|
||||
this.updateTheme(this._themeService.getColorTheme());
|
||||
|
||||
// Load the initial contents of the view model
|
||||
this.viewModel.initialize()
|
||||
.then((accounts: data.Account[]) => {
|
||||
this.updateAccountList(accounts);
|
||||
});
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
super.dispose();
|
||||
if (this._accountList) {
|
||||
this._accountList.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private onAccountSelectionChange(account: data.Account) {
|
||||
this.viewModel.selectedAccount = account;
|
||||
if (account && account.isStale) {
|
||||
new Builder(this._refreshContainer).show();
|
||||
} else {
|
||||
new Builder(this._refreshContainer).hide();
|
||||
}
|
||||
this._onAccountSelectionChangeEvent.fire(account);
|
||||
}
|
||||
|
||||
private renderLabel(container: HTMLElement): IDisposable {
|
||||
if (container.hasChildNodes()) {
|
||||
for (let i = 0; i < container.childNodes.length; i++) {
|
||||
container.removeChild(container.childNodes.item(i));
|
||||
}
|
||||
}
|
||||
|
||||
let selectedAccounts = this._accountList.getSelectedElements();
|
||||
let account = selectedAccounts ? selectedAccounts[0] : null;
|
||||
if (account) {
|
||||
const badge = DOM.$('div.badge');
|
||||
const row = DOM.append(container, DOM.$('div.selected-account-container'));
|
||||
const icon = DOM.append(row, DOM.$('div.icon'));
|
||||
DOM.append(icon, badge);
|
||||
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}')`;
|
||||
// TODO: Pick between the light and dark logo
|
||||
label.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.contextualDisplayName + ')';
|
||||
|
||||
if (account.isStale) {
|
||||
badgeContent.className = 'badge-content icon warning-badge';
|
||||
} else {
|
||||
badgeContent.className = 'badge-content';
|
||||
}
|
||||
} else {
|
||||
const row = DOM.append(container, DOM.$('div.no-account-container'));
|
||||
row.innerText = AddAccountAction.LABEL + '...';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private updateAccountList(accounts: data.Account[]): void {
|
||||
// keep the selection to the current one
|
||||
let selectedElements = this._accountList.getSelectedElements();
|
||||
|
||||
// find selected index
|
||||
let selectedIndex: number;
|
||||
if (selectedElements.length > 0 && accounts.length > 0) {
|
||||
selectedIndex = accounts.findIndex((account) => {
|
||||
return (account.key.accountId === selectedElements[0].key.accountId);
|
||||
});
|
||||
}
|
||||
|
||||
// Replace the existing list with the new one
|
||||
this._accountList.splice(0, this._accountList.length, accounts);
|
||||
|
||||
if (this._accountList.length > 0) {
|
||||
if (selectedIndex && selectedIndex !== -1) {
|
||||
this._accountList.setSelection([selectedIndex]);
|
||||
} else {
|
||||
this._accountList.setSelection([0]);
|
||||
}
|
||||
} else {
|
||||
// if the account is empty, re-render dropdown label
|
||||
this.onAccountSelectionChange(undefined);
|
||||
this._dropdown.renderLabel();
|
||||
}
|
||||
|
||||
this._accountList.layout(this._accountList.contentHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update theming that is specific to account picker
|
||||
*/
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let linkColor = theme.getColor(buttonBackground);
|
||||
let link = linkColor ? linkColor.toString() : null;
|
||||
this._refreshContainer.style.color = link;
|
||||
if (this._refreshContainer) {
|
||||
this._refreshContainer.style.color = link;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import * as data from 'data';
|
||||
|
||||
import { IAccountPickerService } from 'sql/parts/accountManagement/common/interfaces';
|
||||
import { AccountPicker } from 'sql/parts/accountManagement/accountPicker/accountPicker';
|
||||
|
||||
export class AccountPickerService implements IAccountPickerService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private _accountPicker: AccountPicker;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _addAccountCompleteEmitter: Emitter<void>;
|
||||
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
|
||||
|
||||
private _addAccountErrorEmitter: Emitter<string>;
|
||||
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
|
||||
|
||||
private _addAccountStartEmitter: Emitter<void>;
|
||||
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
|
||||
|
||||
private _onAccountSelectionChangeEvent: Emitter<data.Account>;
|
||||
public get onAccountSelectionChangeEvent(): Event<data.Account> { return this._onAccountSelectionChangeEvent.event; }
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
// Create event emitters
|
||||
this._addAccountCompleteEmitter = new Emitter<void>();
|
||||
this._addAccountErrorEmitter = new Emitter<string>();
|
||||
this._addAccountStartEmitter = new Emitter<void>();
|
||||
this._onAccountSelectionChangeEvent = new Emitter<data.Account>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected account
|
||||
*/
|
||||
public get selectedAccount(): data.Account {
|
||||
return this._accountPicker.viewModel.selectedAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render account picker
|
||||
*/
|
||||
public renderAccountPicker(container: HTMLElement): void {
|
||||
if (!this._accountPicker) {
|
||||
// TODO: expand support to multiple providers
|
||||
const providerId: string = 'azurePublicCloud';
|
||||
this._accountPicker = this._instantiationService.createInstance(AccountPicker, providerId);
|
||||
}
|
||||
|
||||
this._accountPicker.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());
|
||||
this._accountPicker.addAccountErrorEvent((msg) => this._addAccountErrorEmitter.fire(msg));
|
||||
this._accountPicker.addAccountStartEvent(() => this._addAccountStartEmitter.fire());
|
||||
this._accountPicker.onAccountSelectionChangeEvent((account) => this._onAccountSelectionChangeEvent.fire(account));
|
||||
this._accountPicker.render(container);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as data from 'data';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
|
||||
import { UpdateAccountListEventParams } from 'sql/services/accountManagement/eventTypes';
|
||||
|
||||
/**
|
||||
* View model for account picker
|
||||
*/
|
||||
export class AccountPickerViewModel {
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _updateAccountListEmitter: Emitter<UpdateAccountListEventParams>;
|
||||
public get updateAccountListEvent(): Event<UpdateAccountListEventParams> { return this._updateAccountListEmitter.event; }
|
||||
|
||||
public selectedAccount: data.Account;
|
||||
|
||||
constructor(
|
||||
private _providerId: string,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService
|
||||
) {
|
||||
let self = this;
|
||||
|
||||
// Create our event emitters
|
||||
this._updateAccountListEmitter = new Emitter<UpdateAccountListEventParams>();
|
||||
|
||||
// Register handlers for any changes to the accounts
|
||||
this._accountManagementService.updateAccountListEvent(arg => self._updateAccountListEmitter.fire(arg));
|
||||
}
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
/**
|
||||
* Loads an initial list of accounts from the account management service
|
||||
* @return {Thenable<Account[]>} Promise to return the list of accounts
|
||||
*/
|
||||
public initialize(): Thenable<data.Account[]> {
|
||||
// Load a baseline of the accounts for the provider
|
||||
return this._accountManagementService.getAccountsForProvider(this._providerId)
|
||||
.then(null, () => {
|
||||
// In the event we failed to lookup accounts for the provider, just send
|
||||
// back an empty collection
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Selected account */
|
||||
.selected-account-container {
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.selected-account-container .icon {
|
||||
flex: 0 0 25px;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.selected-account-container .label {
|
||||
flex: 1 1 auto;
|
||||
padding-left: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.selected-account-container .icon {
|
||||
background-size: 25px;
|
||||
}
|
||||
|
||||
.selected-account-container .icon .badge {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
overflow: hidden;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.selected-account-container .icon .badge .badge-content {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-size: 12px;
|
||||
}
|
||||
|
||||
/* A container when the account list is empty */
|
||||
.no-account-container {
|
||||
padding: 6px;
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Account list */
|
||||
.account-list-container .list-row {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.account-list-container .list-row .icon {
|
||||
flex: 0 0 35px;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
background-size: 35px;
|
||||
}
|
||||
|
||||
.account-list-container .list-row .icon .badge {
|
||||
position: relative;
|
||||
top: 22px;
|
||||
left: 22px;
|
||||
overflow: hidden;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.account-list-container .list-row .icon .badge .badge-content {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-size: 15px;
|
||||
}
|
||||
|
||||
/* Refresh link */
|
||||
.refresh-container {
|
||||
padding-left: 15px;
|
||||
padding-top: 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.refresh-container .monaco-action-bar .actions-container {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.refresh-container .icon {
|
||||
flex: 0 0 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.refresh-container .monaco-action-bar {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
158
src/sql/parts/accountManagement/common/accountActions.ts
Normal file
158
src/sql/parts/accountManagement/common/accountActions.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as data from 'data';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IMessageService, IConfirmation, Severity } from 'vs/platform/message/common/message';
|
||||
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
|
||||
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
|
||||
|
||||
/**
|
||||
* Actions to add a new account
|
||||
*/
|
||||
export class AddAccountAction extends Action {
|
||||
// CONSTANTS ///////////////////////////////////////////////////////////
|
||||
public static ID = 'account.addLinkedAccount';
|
||||
public static LABEL = localize('addAccount', 'Add an account');
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _addAccountCompleteEmitter: Emitter<void>;
|
||||
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
|
||||
|
||||
private _addAccountErrorEmitter: Emitter<string>;
|
||||
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
|
||||
|
||||
private _addAccountStartEmitter: Emitter<void>;
|
||||
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
|
||||
|
||||
constructor(
|
||||
private _providerId: string,
|
||||
@IMessageService private _messageService: IMessageService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService
|
||||
) {
|
||||
super(AddAccountAction.ID, AddAccountAction.LABEL);
|
||||
this.class = 'add-linked-account-action';
|
||||
|
||||
this._addAccountCompleteEmitter = new Emitter<void>();
|
||||
this._addAccountErrorEmitter = new Emitter<string>();
|
||||
this._addAccountStartEmitter = new Emitter<void>();
|
||||
}
|
||||
|
||||
public run(): TPromise<boolean> {
|
||||
let self = this;
|
||||
|
||||
// Fire the event that we've started adding accounts
|
||||
this._addAccountStartEmitter.fire();
|
||||
|
||||
return new TPromise((resolve, reject) => {
|
||||
self._accountManagementService.addAccount(self._providerId)
|
||||
.then(
|
||||
() => {
|
||||
self._addAccountCompleteEmitter.fire();
|
||||
resolve(true);
|
||||
},
|
||||
err => {
|
||||
error(`Error while adding account: ${err}`);
|
||||
self._addAccountErrorEmitter.fire(err);
|
||||
self._addAccountCompleteEmitter.fire();
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions to remove the account
|
||||
*/
|
||||
export class RemoveAccountAction extends Action {
|
||||
public static ID = 'account.removeAccount';
|
||||
public static LABEL = localize('removeAccount', 'Remove account');
|
||||
|
||||
constructor(
|
||||
private _account: data.Account,
|
||||
@IMessageService private _messageService: IMessageService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService
|
||||
) {
|
||||
super(RemoveAccountAction.ID, RemoveAccountAction.LABEL, 'remove-account-action icon remove');
|
||||
}
|
||||
|
||||
public run(): TPromise<boolean> {
|
||||
let self = this;
|
||||
|
||||
// Ask for Confirm
|
||||
let confirm: IConfirmation = {
|
||||
message: localize('confirmRemoveUserAccountMessage', "Are you sure you want to remove '{0}'?", this._account.displayInfo.displayName),
|
||||
primaryButton: localize('yes', 'Yes'),
|
||||
secondaryButton: localize('no', 'No'),
|
||||
type: 'question'
|
||||
};
|
||||
|
||||
if (!this._messageService.confirm(confirm)) {
|
||||
return TPromise.as(false);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions to apply filter to the account
|
||||
*/
|
||||
export class ApplyFilterAction extends Action {
|
||||
public static ID = 'account.applyFilters';
|
||||
public static LABEL = localize('applyFilters', 'Apply Filters');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string
|
||||
) {
|
||||
super(id, label, 'apply-filters-action icon filter');
|
||||
}
|
||||
|
||||
public run(): TPromise<boolean> {
|
||||
// Todo: apply filter to the account
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions to refresh the account
|
||||
*/
|
||||
export class RefreshAccountAction extends Action {
|
||||
public static ID = 'account.refresh';
|
||||
public static LABEL = localize('refreshAccount', 'Reenter your credentials');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string
|
||||
) {
|
||||
super(id, label, 'refresh-account-action icon refresh');
|
||||
}
|
||||
public run(): TPromise<boolean> {
|
||||
// Todo: refresh the account
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
125
src/sql/parts/accountManagement/common/accountListRenderer.ts
Normal file
125
src/sql/parts/accountManagement/common/accountListRenderer.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!sql/parts/accountManagement/common/media/accountListRenderer';
|
||||
import 'vs/css!sql/parts/accountManagement/common/media/accountActions';
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { ActionBar, IActionOptions } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { RemoveAccountAction, ApplyFilterAction, RefreshAccountAction } from 'sql/parts/accountManagement/common/accountActions';
|
||||
|
||||
import * as data from 'data';
|
||||
|
||||
export class AccountListDelegate implements IDelegate<data.Account> {
|
||||
|
||||
constructor(
|
||||
private _height: number
|
||||
) {
|
||||
}
|
||||
|
||||
public getHeight(element: data.Account): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public getTemplateId(element: data.Account): string {
|
||||
return 'accountListRenderer';
|
||||
}
|
||||
}
|
||||
|
||||
export interface AccountListTemplate {
|
||||
root: HTMLElement;
|
||||
icon: HTMLElement;
|
||||
badgeContent: HTMLElement;
|
||||
contextualDisplayName: HTMLElement;
|
||||
label: HTMLElement;
|
||||
displayName: HTMLElement;
|
||||
content?: HTMLElement;
|
||||
actions?: ActionBar;
|
||||
}
|
||||
|
||||
export class AccountPickerListRenderer implements IRenderer<data.Account, AccountListTemplate> {
|
||||
public static TEMPLATE_ID = 'accountListRenderer';
|
||||
|
||||
public get templateId(): string {
|
||||
return AccountPickerListRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
public renderTemplate(container: HTMLElement): AccountListTemplate {
|
||||
const tableTemplate: AccountListTemplate = Object.create(null);
|
||||
const badge = DOM.$('div.badge');
|
||||
tableTemplate.root = DOM.append(container, DOM.$('div.list-row.account-picker-list'));
|
||||
tableTemplate.icon = DOM.append(tableTemplate.root, DOM.$('div.icon'));
|
||||
DOM.append(tableTemplate.icon, badge);
|
||||
tableTemplate.badgeContent = DOM.append(badge, DOM.$('div.badge-content'));
|
||||
tableTemplate.label = DOM.append(tableTemplate.root, DOM.$('div.label'));
|
||||
tableTemplate.contextualDisplayName = DOM.append(tableTemplate.label, DOM.$('div.contextual-display-name'));
|
||||
tableTemplate.displayName = DOM.append(tableTemplate.label, DOM.$('div.display-name'));
|
||||
return tableTemplate;
|
||||
}
|
||||
|
||||
public renderElement(account: 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.contextualDisplayName.innerText = account.displayInfo.contextualDisplayName;
|
||||
templateData.displayName.innerText = account.displayInfo.displayName;
|
||||
if (account.isStale) {
|
||||
templateData.badgeContent.className = 'badge-content icon warning-badge';
|
||||
} else {
|
||||
templateData.badgeContent.className = 'badge-content';
|
||||
}
|
||||
}
|
||||
|
||||
public disposeTemplate(template: AccountListTemplate): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountListRenderer extends AccountPickerListRenderer {
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get templateId(): string {
|
||||
return AccountListRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
public renderTemplate(container: HTMLElement): AccountListTemplate {
|
||||
const tableTemplate = super.renderTemplate(container);
|
||||
tableTemplate.content = DOM.append(tableTemplate.label, DOM.$('div.content'));
|
||||
tableTemplate.actions = new ActionBar(tableTemplate.root, { animated: false });
|
||||
|
||||
return tableTemplate;
|
||||
}
|
||||
|
||||
public renderElement(account: data.Account, index: number, templateData: AccountListTemplate): void {
|
||||
super.renderElement(account, index, templateData);
|
||||
if (account.isStale) {
|
||||
templateData.content.innerText = localize('refreshCredentials', 'You need to refresh the credentials for this account.');
|
||||
} else {
|
||||
templateData.content.innerText = '';
|
||||
}
|
||||
templateData.actions.clear();
|
||||
|
||||
let actionOptions: IActionOptions = { icon: true, label: false };
|
||||
if (account.isStale) {
|
||||
templateData.actions.push(new RefreshAccountAction(RefreshAccountAction.ID, RefreshAccountAction.LABEL), actionOptions);
|
||||
} else {
|
||||
templateData.actions.push(new ApplyFilterAction(ApplyFilterAction.ID, ApplyFilterAction.LABEL), actionOptions);
|
||||
}
|
||||
|
||||
let removeAction = this._instantiationService.createInstance(RemoveAccountAction, account);
|
||||
templateData.actions.push(removeAction, actionOptions);
|
||||
}
|
||||
}
|
||||
61
src/sql/parts/accountManagement/common/interfaces.ts
Normal file
61
src/sql/parts/accountManagement/common/interfaces.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import Event from 'vs/base/common/event';
|
||||
import * as data from 'data';
|
||||
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
|
||||
export const SERVICE_ID = 'resourceProviderService';
|
||||
export const IResourceProviderService = createDecorator<IResourceProviderService>(SERVICE_ID);
|
||||
|
||||
export interface IHandleFirewallRuleResult {
|
||||
canHandleFirewallRule: boolean;
|
||||
ipAddress: string;
|
||||
resourceProviderId: string;
|
||||
}
|
||||
|
||||
export interface IResourceProviderService {
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
* Register a resource provider
|
||||
*/
|
||||
registerProvider(providerId: string, provider: data.ResourceProvider): void;
|
||||
|
||||
/**
|
||||
* Unregister a resource provider
|
||||
*/
|
||||
unregisterProvider(ProviderId: string): void;
|
||||
|
||||
/**
|
||||
* Create a firewall rule
|
||||
*/
|
||||
createFirewallRule(selectedAccount: data.Account, firewallruleInfo: data.FirewallRuleInfo, resourceProviderId: string): Promise<data.CreateFirewallRuleResponse>;
|
||||
|
||||
/**
|
||||
* handle a firewall rule
|
||||
*/
|
||||
handleFirewallRule(errorCode: number, errorMessage: string, connectionTypeId: string): Promise<IHandleFirewallRuleResult>;
|
||||
|
||||
/**
|
||||
* Show firewall rule dialog
|
||||
*/
|
||||
showFirewallRuleDialog(connection: IConnectionProfile, ipAddress: string, resourceProviderId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export const IAccountPickerService = createDecorator<IAccountPickerService>('AccountPickerService');
|
||||
export interface IAccountPickerService {
|
||||
_serviceBrand: any;
|
||||
renderAccountPicker(container: HTMLElement): void;
|
||||
addAccountCompleteEvent: Event<void>;
|
||||
addAccountErrorEvent: Event<string>;
|
||||
addAccountStartEvent: Event<void>;
|
||||
onAccountSelectionChangeEvent: Event<data.Account>;
|
||||
selectedAccount: data.Account;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Icons for various account actions */
|
||||
.vs .action-item .icon.add-linked-account-action {
|
||||
background-image: url('new_account.svg');
|
||||
}
|
||||
|
||||
.vs-dark .action-item .icon.add-linked-account-action,
|
||||
.hc-black .action-item .icon.add-linked-account-action {
|
||||
background-image: url('new_account_inverse.svg');
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.list-row.account-picker-list {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.list-row.account-picker-list .label {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.list-row.account-picker-list .label .contextual-display-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.list-row.account-picker-list .label .content {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.account-logo {
|
||||
background: no-repeat center center;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>new_account_16x16</title><path d="M8.09,7.39a5.91,5.91,0,0,1,1.38.74,6.06,6.06,0,0,1,1.13,1.06A6,6,0,0,1,11.88,12h-1a4.94,4.94,0,0,0-.63-1.61A5,5,0,0,0,7.66,8.3a5,5,0,0,0-3-.12,5.09,5.09,0,0,0-1.2.5,5,5,0,0,0-1.79,1.79,5.07,5.07,0,0,0-.5,1.2A4.88,4.88,0,0,0,1,13H0a5.88,5.88,0,0,1,.28-1.8A6,6,0,0,1,1,9.59a6.1,6.1,0,0,1,1.22-1.3,5.8,5.8,0,0,1,1.59-.9A4.2,4.2,0,0,1,2.46,5.94,3.86,3.86,0,0,1,2,4a3.92,3.92,0,0,1,.31-1.56A4,4,0,0,1,4.4.31a4,4,0,0,1,3.12,0A4,4,0,0,1,9.65,2.44,4,4,0,0,1,9.83,5a4,4,0,0,1-1,1.74A3.94,3.94,0,0,1,8.09,7.39ZM3,4A2.92,2.92,0,0,0,3.2,5.17a3,3,0,0,0,1.6,1.6,3,3,0,0,0,2.33,0,3,3,0,0,0,1.6-1.6,3,3,0,0,0,0-2.33,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.92,2.92,0,0,0,3,4Zm13,9v1H14v2H13V14H11V13h2V11h1v2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 850 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>new_account_inverse_16x16</title><path class="cls-1" d="M8.13,7.39a5.91,5.91,0,0,1,1.38.74,6.06,6.06,0,0,1,1.13,1.06A6,6,0,0,1,11.91,12h-1a4.94,4.94,0,0,0-.63-1.61A5,5,0,0,0,7.7,8.3a5,5,0,0,0-3-.12,5.09,5.09,0,0,0-1.2.5,5,5,0,0,0-1.79,1.79,5.07,5.07,0,0,0-.5,1.2A4.88,4.88,0,0,0,1,13H0a5.88,5.88,0,0,1,.28-1.8,6,6,0,0,1,.79-1.6,6.1,6.1,0,0,1,1.22-1.3,5.8,5.8,0,0,1,1.59-.9A4.2,4.2,0,0,1,2.5,5.94,3.86,3.86,0,0,1,2,4a3.92,3.92,0,0,1,.31-1.56A4,4,0,0,1,4.44.31a4,4,0,0,1,3.12,0A4,4,0,0,1,9.69,2.44,4,4,0,0,1,9.87,5a4,4,0,0,1-1,1.74A3.94,3.94,0,0,1,8.13,7.39ZM3,4a2.92,2.92,0,0,0,.23,1.17,3,3,0,0,0,1.6,1.6,3,3,0,0,0,2.33,0,3,3,0,0,0,1.6-1.6,3,3,0,0,0,0-2.33,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.92,2.92,0,0,0,3,4Zm13,9v1H14v2H13V14H11V13h2V11h1v2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 918 B |
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'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';
|
||||
import * as Constants from 'sql/common/constants';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
|
||||
import { FirewallRuleDialogController } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialogController';
|
||||
|
||||
import * as data from 'data';
|
||||
|
||||
export class ResourceProviderService implements IResourceProviderService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
private _providers: { [handle: string]: data.ResourceProvider; } = Object.create(null);
|
||||
private _firewallRuleDialogController: FirewallRuleDialogController;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private _telemetryService: ITelemetryService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IEnvironmentService private _environmentService: IEnvironmentService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the firewall rule dialog
|
||||
*/
|
||||
public showFirewallRuleDialog(connection: IConnectionProfile, ipAddress: string, resourceProviderId: string): Promise<boolean> {
|
||||
let self = this;
|
||||
// If the firewall rule dialog hasn't been defined, create a new one
|
||||
if (!self._firewallRuleDialogController) {
|
||||
self._firewallRuleDialogController = self._instantiationService.createInstance(FirewallRuleDialogController);
|
||||
}
|
||||
|
||||
return self._firewallRuleDialogController.openFirewallRuleDialog(connection, ipAddress, resourceProviderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a firewall rule
|
||||
*/
|
||||
public createFirewallRule(selectedAccount: data.Account, firewallruleInfo: data.FirewallRuleInfo, resourceProviderId: string): Promise<data.CreateFirewallRuleResponse> {
|
||||
return new Promise<data.CreateFirewallRuleResponse>((resolve, reject) => {
|
||||
let provider = this._providers[resourceProviderId];
|
||||
if (provider) {
|
||||
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.FirewallRuleRequested, { provider: resourceProviderId });
|
||||
provider.createFirewallRule(selectedAccount, firewallruleInfo).then(result => {
|
||||
resolve(result);
|
||||
}, error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
reject(Constants.InvalidProvider);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 */
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a resource provider
|
||||
*/
|
||||
public registerProvider(providerId: string, provider: data.ResourceProvider): void {
|
||||
this._providers[providerId] = provider;
|
||||
}
|
||||
|
||||
public unregisterProvider(providerId: string): void {
|
||||
delete this._providers[providerId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/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 { 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 { Modal } from 'sql/base/browser/ui/modal/modal';
|
||||
import { FirewallRuleViewModel } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleViewModel';
|
||||
import { attachModalDialogStyler } 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';
|
||||
|
||||
export class FirewallRuleDialog extends Modal {
|
||||
public viewModel: FirewallRuleViewModel;
|
||||
private _createButton: Button;
|
||||
private _closeButton: Button;
|
||||
private _fromRangeinputBox: InputBox;
|
||||
private _toRangeinputBox: InputBox;
|
||||
|
||||
private _helpLink: HTMLElement;
|
||||
private _IPAddressInput: HTMLElement;
|
||||
private _subnetIPRangeInput: HTMLElement;
|
||||
private _IPAddressElement: HTMLElement;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _onAddAccountErrorEmitter: Emitter<string>;
|
||||
public get onAddAccountErrorEvent(): Event<string> { return this._onAddAccountErrorEmitter.event; }
|
||||
|
||||
private _onCancel: Emitter<void>;
|
||||
public get onCancel(): Event<void> { return this._onCancel.event; }
|
||||
|
||||
private _onCreateFirewallRule: Emitter<void>;
|
||||
public get onCreateFirewallRule(): Event<void> { return this._onCreateFirewallRule.event; }
|
||||
|
||||
constructor(
|
||||
@IAccountPickerService private _accountPickerService: IAccountPickerService,
|
||||
@IPartService partService: IPartService,
|
||||
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(
|
||||
localize('createNewFirewallRule', 'Create new firewall rule'),
|
||||
TelemetryKeys.FireWallRule,
|
||||
partService,
|
||||
telemetryService,
|
||||
contextKeyService,
|
||||
{
|
||||
isFlyout: true,
|
||||
hasBackButton: true,
|
||||
hasSpinner: true
|
||||
}
|
||||
);
|
||||
|
||||
// Setup event emitters
|
||||
this._onAddAccountErrorEmitter = new Emitter<string>();
|
||||
this._onCancel = new Emitter<void>();
|
||||
this._onCreateFirewallRule = new Emitter<void>();
|
||||
|
||||
this.viewModel = this._instantiationService.createInstance(FirewallRuleViewModel);
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
this.backButton.addListener('click', () => this.cancel());
|
||||
this._register(attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }));
|
||||
this._createButton = this.addFooterButton(localize('ok', 'OK'), () => this.createFirewallRule());
|
||||
this._closeButton = this.addFooterButton(localize('cancel', 'Cancel'), () => this.cancel());
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
let descriptionSection;
|
||||
$().div({ class: 'firewall-rule-description-section new-section' }, (descriptionContainer) => {
|
||||
descriptionSection = descriptionContainer.getHTMLElement();
|
||||
DOM.append(descriptionContainer.getHTMLElement(), DOM.$('div.firewall-rule-icon'));
|
||||
|
||||
const textDescriptionContainer = DOM.append(descriptionContainer.getHTMLElement(), DOM.$('div.firewall-rule-description'));
|
||||
let dialogDescription = localize('firewallRuleDialogDescription',
|
||||
'Your client IP address does not have access to the server. Sign in to an Azure account and create a new firewall rule to enable access.');
|
||||
this.createLabelElement(new Builder(textDescriptionContainer), dialogDescription, false);
|
||||
this._helpLink = DOM.append(textDescriptionContainer, DOM.$('a.help-link'));
|
||||
this._helpLink.setAttribute('href', 'https://docs.microsoft.com/en-us/azure/sql-database/sql-database-firewall-configure');
|
||||
this._helpLink.innerHTML += localize('firewallRuleHelpDescription', 'Learn more about firewall settings');
|
||||
});
|
||||
|
||||
// Create account picker with event handling
|
||||
this._accountPickerService.addAccountCompleteEvent(() => this.hideSpinner());
|
||||
this._accountPickerService.addAccountErrorEvent((msg) => {
|
||||
this.hideSpinner();
|
||||
this._onAddAccountErrorEmitter.fire(msg);
|
||||
});
|
||||
this._accountPickerService.addAccountStartEvent(() => this.showSpinner());
|
||||
this._accountPickerService.onAccountSelectionChangeEvent((account) => this.onAccountSelectionChange(account));
|
||||
|
||||
let azureAccountSection;
|
||||
$().div({ class: 'azure-account-section new-section' }, (azureAccountContainer) => {
|
||||
azureAccountSection = azureAccountContainer.getHTMLElement();
|
||||
let azureAccountLabel = localize('azureAccount', 'Azure account');
|
||||
this.createLabelElement(azureAccountContainer, azureAccountLabel, true);
|
||||
azureAccountContainer.div({ class: 'dialog-input' }, (inputCellContainer) => {
|
||||
this._accountPickerService.renderAccountPicker(inputCellContainer.getHTMLElement());
|
||||
});
|
||||
});
|
||||
|
||||
let subnetIPRangeSection;
|
||||
$().div({ class: 'subnet-ip-range-input' }, (subnetIPRangeContainer) => {
|
||||
subnetIPRangeSection = subnetIPRangeContainer.getHTMLElement();
|
||||
subnetIPRangeContainer.div({ class: 'dialog-input-section' }, (inputContainer) => {
|
||||
inputContainer.div({ class: 'dialog-label' }, (labelContainer) => {
|
||||
labelContainer.innerHtml(localize('from', 'From'));
|
||||
});
|
||||
|
||||
inputContainer.div({ class: 'dialog-input' }, (inputCellContainer) => {
|
||||
this._fromRangeinputBox = new InputBox(inputCellContainer.getHTMLElement(), this._contextViewService);
|
||||
});
|
||||
|
||||
inputContainer.div({ class: 'dialog-label' }, (labelContainer) => {
|
||||
labelContainer.innerHtml(localize('to', 'To'));
|
||||
});
|
||||
|
||||
inputContainer.div({ class: 'dialog-input' }, (inputCellContainer) => {
|
||||
this._toRangeinputBox = new InputBox(inputCellContainer.getHTMLElement(), this._contextViewService);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let firewallRuleSection;
|
||||
$().div({ class: 'firewall-rule-section new-section' }, (firewallRuleContainer) => {
|
||||
firewallRuleSection = firewallRuleContainer.getHTMLElement();
|
||||
let firewallRuleLabel = localize('filewallRule', 'Firewall rule');
|
||||
this.createLabelElement(firewallRuleContainer, firewallRuleLabel, true);
|
||||
firewallRuleContainer.div({ class: 'radio-section' }, (radioContainer) => {
|
||||
const form = DOM.append(radioContainer.getHTMLElement(), DOM.$('form.firewall-rule'));
|
||||
const IPAddressDiv = DOM.append(form, DOM.$('div.firewall-ip-address dialog-input'));
|
||||
const subnetIPRangeDiv = DOM.append(form, DOM.$('div.firewall-subnet-ip-range dialog-input'));
|
||||
|
||||
const IPAddressContainer = DOM.append(IPAddressDiv, DOM.$('div.option-container'));
|
||||
this._IPAddressInput = DOM.append(IPAddressContainer, DOM.$('input.option-input'));
|
||||
this._IPAddressInput.setAttribute('type', 'radio');
|
||||
this._IPAddressInput.setAttribute('name', 'firewallRuleChoice');
|
||||
this._IPAddressInput.setAttribute('value', 'ipAddress');
|
||||
const IPAddressDescription = DOM.append(IPAddressContainer, DOM.$('div.option-description'));
|
||||
IPAddressDescription.innerText = localize('addIPAddressLabel', 'Add my client IP ');
|
||||
this._IPAddressElement = DOM.append(IPAddressContainer, DOM.$('div.option-ip-address'));
|
||||
|
||||
const subnetIpRangeContainer = DOM.append(subnetIPRangeDiv, DOM.$('div.option-container'));
|
||||
this._subnetIPRangeInput = DOM.append(subnetIpRangeContainer, DOM.$('input.option-input'));
|
||||
this._subnetIPRangeInput.setAttribute('type', 'radio');
|
||||
this._subnetIPRangeInput.setAttribute('name', 'firewallRuleChoice');
|
||||
this._subnetIPRangeInput.setAttribute('value', 'ipRange');
|
||||
const subnetIPRangeDescription = DOM.append(subnetIpRangeContainer, DOM.$('div.option-description'));
|
||||
subnetIPRangeDescription.innerText = localize('addIpRangeLabel', 'Add my subnet IP range');
|
||||
DOM.append(subnetIPRangeDiv, subnetIPRangeSection);
|
||||
});
|
||||
});
|
||||
|
||||
new Builder(container).div({ class: 'firewall-rule-dialog' }, (builder) => {
|
||||
builder.append(descriptionSection);
|
||||
builder.append(azureAccountSection);
|
||||
builder.append(firewallRuleSection);
|
||||
});
|
||||
|
||||
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
|
||||
this.updateTheme(this._themeService.getColorTheme());
|
||||
|
||||
jQuery(this._IPAddressInput).on('click', () => {
|
||||
this.onFirewallRuleOptionSelected(true);
|
||||
});
|
||||
|
||||
jQuery(this._subnetIPRangeInput).on('click', () => {
|
||||
this.onFirewallRuleOptionSelected(false);
|
||||
});
|
||||
}
|
||||
|
||||
private onFirewallRuleOptionSelected(isIPAddress: boolean) {
|
||||
this.viewModel.isIPAddressSelected = isIPAddress;
|
||||
if (this._fromRangeinputBox) {
|
||||
isIPAddress ? this._fromRangeinputBox.disable() : this._fromRangeinputBox.enable();
|
||||
}
|
||||
if (this._toRangeinputBox) {
|
||||
isIPAddress ? this._toRangeinputBox.disable() : this._toRangeinputBox.enable();
|
||||
}
|
||||
}
|
||||
|
||||
protected layout(height?: number): void {
|
||||
// Nothing currently laid out statically in this class
|
||||
}
|
||||
|
||||
private createLabelElement(container: Builder, content: string, isHeader?: boolean) {
|
||||
let className = 'dialog-label';
|
||||
if (isHeader) {
|
||||
className += ' header';
|
||||
}
|
||||
container.div({ class: className }, (labelContainer) => {
|
||||
labelContainer.innerHtml(content);
|
||||
});
|
||||
}
|
||||
|
||||
// Update theming that is specific to firewall rule flyout body
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let linkColor = theme.getColor(buttonBackground);
|
||||
let link = linkColor ? linkColor.toString() : null;
|
||||
this._helpLink.style.color = link;
|
||||
if (this._helpLink) {
|
||||
this._helpLink.style.color = link;
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(attachButtonStyler(this._createButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._closeButton, this._themeService));
|
||||
this._register(attachInputBoxStyler(this._fromRangeinputBox, this._themeService));
|
||||
this._register(attachInputBoxStyler(this._toRangeinputBox, this._themeService));
|
||||
|
||||
// handler for from subnet ip range change events
|
||||
this._register(this._fromRangeinputBox.onDidChange(IPAddress => {
|
||||
this.fromRangeInputChanged(IPAddress);
|
||||
}));
|
||||
|
||||
// handler for to subnet ip range change events
|
||||
this._register(this._toRangeinputBox.onDidChange(IPAddress => {
|
||||
this.toRangeInputChanged(IPAddress);
|
||||
}));
|
||||
}
|
||||
|
||||
private fromRangeInputChanged(IPAddress: string) {
|
||||
this.viewModel.fromSubnetIPRange = IPAddress;
|
||||
}
|
||||
|
||||
private toRangeInputChanged(IPAddress: string) {
|
||||
this.viewModel.toSubnetIPRange = IPAddress;
|
||||
}
|
||||
|
||||
/* Overwrite esapce key behavior */
|
||||
protected onClose() {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept() {
|
||||
this.createFirewallRule();
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
this._onCancel.fire();
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public createFirewallRule() {
|
||||
if (this._createButton.enabled) {
|
||||
this._createButton.enabled = false;
|
||||
this.showSpinner();
|
||||
this._onCreateFirewallRule.fire();
|
||||
}
|
||||
}
|
||||
|
||||
public onAccountSelectionChange(account: data.Account): void {
|
||||
this.viewModel.selectedAccount = account;
|
||||
if (account && !account.isStale) {
|
||||
this._createButton.enabled = true;
|
||||
} else {
|
||||
this._createButton.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onServiceComplete() {
|
||||
this._createButton.enabled = true;
|
||||
this.hideSpinner();
|
||||
}
|
||||
|
||||
public open() {
|
||||
this._IPAddressInput.click();
|
||||
this.onAccountSelectionChange(this._accountPickerService.selectedAccount);
|
||||
this._fromRangeinputBox.setPlaceHolder(this.viewModel.defaultFromSubnetIPRange);
|
||||
this._toRangeinputBox.setPlaceHolder(this.viewModel.defaultToSubnetIPRange);
|
||||
this._IPAddressElement.innerText = '(' + this.viewModel.defaultIPAddress + ')';
|
||||
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
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 { FirewallRuleDialog } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog';
|
||||
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
|
||||
import { IResourceProviderService } from 'sql/parts/accountManagement/common/interfaces';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
export class FirewallRuleDialogController {
|
||||
|
||||
private _firewallRuleDialog: FirewallRuleDialog;
|
||||
private _connection: IConnectionProfile;
|
||||
private _resourceProviderId: string;
|
||||
|
||||
private _addAccountErrorTitle = localize('addAccountErrorTitle', 'Error adding account');
|
||||
private _firewallRuleErrorTitle = localize('firewallRuleError', 'Firewall rule error');
|
||||
private _deferredPromise: Deferred<boolean>;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
|
||||
@IConnectionManagementService private _connectionService: IConnectionManagementService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open firewall rule dialog
|
||||
*/
|
||||
public openFirewallRuleDialog(connection: IConnectionProfile, ipAddress: string, resourceProviderId: string): Promise<boolean> {
|
||||
if (!this._firewallRuleDialog) {
|
||||
this._firewallRuleDialog = this._instantiationService.createInstance(FirewallRuleDialog);
|
||||
this._firewallRuleDialog.onCancel(this.handleOnCancel, this);
|
||||
this._firewallRuleDialog.onCreateFirewallRule(this.handleOnCreateFirewallRule, this);
|
||||
this._firewallRuleDialog.onAddAccountErrorEvent(this.handleOnAddAccountError, this);
|
||||
this._firewallRuleDialog.render();
|
||||
}
|
||||
this._connection = connection;
|
||||
this._resourceProviderId = resourceProviderId;
|
||||
this._firewallRuleDialog.viewModel.updateDefaultValues(ipAddress);
|
||||
this._firewallRuleDialog.open();
|
||||
this._deferredPromise = new Deferred();
|
||||
return this._deferredPromise.promise;
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private handleOnAddAccountError(message: string): void {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._addAccountErrorTitle, message);
|
||||
}
|
||||
|
||||
private handleOnCreateFirewallRule(): void {
|
||||
let resourceProviderId = this._resourceProviderId;
|
||||
|
||||
this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount).then(tokenMappings => {
|
||||
let firewallRuleInfo: data.FirewallRuleInfo = {
|
||||
startIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.fromSubnetIPRange,
|
||||
endIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.toSubnetIPRange,
|
||||
serverName: this._connection.serverName,
|
||||
securityTokenMappings: tokenMappings
|
||||
};
|
||||
|
||||
this._resourceProviderService.createFirewallRule(this._firewallRuleDialog.viewModel.selectedAccount, firewallRuleInfo, resourceProviderId).then(createFirewallRuleResponse => {
|
||||
if (createFirewallRuleResponse.result) {
|
||||
this._firewallRuleDialog.close();
|
||||
this._deferredPromise.resolve(true);
|
||||
} else {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._firewallRuleErrorTitle, createFirewallRuleResponse.errorMessage);
|
||||
}
|
||||
this._firewallRuleDialog.onServiceComplete();
|
||||
}, error => {
|
||||
this.showError(error);
|
||||
});
|
||||
}, error => {
|
||||
this.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
private showError(error: any): void {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._firewallRuleErrorTitle, error);
|
||||
this._firewallRuleDialog.onServiceComplete();
|
||||
// Note: intentionally not rejecting the promise as we want users to be able to choose a different account
|
||||
}
|
||||
|
||||
private handleOnCancel(): void {
|
||||
this._deferredPromise.resolve(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as data from 'data';
|
||||
|
||||
/**
|
||||
* View model for firewall rule dialog
|
||||
*/
|
||||
export class FirewallRuleViewModel {
|
||||
public isIPAddressSelected: boolean;
|
||||
public selectedAccount: data.Account;
|
||||
|
||||
private _defaultIPAddress: string;
|
||||
private _defaultFromSubnetIPRange: string;
|
||||
private _defaultToSubnetIPRange: string;
|
||||
private _fromSubnetIPRange: string;
|
||||
private _toSubnetIPRange: string;
|
||||
|
||||
constructor() {
|
||||
this.isIPAddressSelected = true;
|
||||
}
|
||||
|
||||
public get defaultIPAddress(): string {
|
||||
return this._defaultIPAddress;
|
||||
}
|
||||
|
||||
public get defaultFromSubnetIPRange(): string {
|
||||
return this._defaultFromSubnetIPRange;
|
||||
}
|
||||
|
||||
public get defaultToSubnetIPRange(): string {
|
||||
return this._defaultToSubnetIPRange;
|
||||
}
|
||||
|
||||
public set fromSubnetIPRange(IPAddress: string) {
|
||||
this._fromSubnetIPRange = IPAddress;
|
||||
}
|
||||
|
||||
public get fromSubnetIPRange(): string {
|
||||
if (this._fromSubnetIPRange) {
|
||||
return this._fromSubnetIPRange;
|
||||
} else {
|
||||
return this._defaultFromSubnetIPRange;
|
||||
}
|
||||
}
|
||||
|
||||
public set toSubnetIPRange(IPAddress: string) {
|
||||
this._toSubnetIPRange = IPAddress;
|
||||
}
|
||||
|
||||
public get toSubnetIPRange(): string {
|
||||
if (this._toSubnetIPRange) {
|
||||
return this._toSubnetIPRange;
|
||||
} else {
|
||||
return this._defaultToSubnetIPRange;
|
||||
}
|
||||
}
|
||||
|
||||
public updateDefaultValues(ipAddress: string): void {
|
||||
this._defaultIPAddress = ipAddress;
|
||||
this._defaultFromSubnetIPRange = ipAddress.replace(/\.[0-9]+$/g, '.0');
|
||||
this._defaultToSubnetIPRange = ipAddress.replace(/\.[0-9]+$/g, '.255');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.firewall-rule-dialog {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-label.header {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .new-section {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input-section {
|
||||
display: flex;
|
||||
padding-left: 30px;
|
||||
padding-right: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input-section .dialog-label {
|
||||
flex: 0 0 15px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input-section .dialog-input {
|
||||
flex: 0 0 100px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .azure-account-section {
|
||||
height: 92px;
|
||||
}
|
||||
|
||||
/* Firewall rule description section */
|
||||
.modal .firewall-rule-dialog a:link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-description-section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-description-section .firewall-rule-icon {
|
||||
background: url('secure.svg') center center no-repeat;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-description-section .firewall-rule-description {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
/* Radio button section*/
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-input {
|
||||
margin-top: 0px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-description {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-ip-address {
|
||||
padding-left: 3px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36.65 50"><defs><style>.cls-1{fill:#7fba00;}.cls-2{fill:#b8d432;opacity:0.4;isolation:isolate;}.cls-3{fill:#fff;}</style></defs><title>secure</title><path class="cls-1" d="M31.19,6h0c-11-1.22-12.86-6-12.86-6S15.85,6.24,0,6.24V31.85c0,3.1,1.72,6,4.1,8.53h0C9.51,46.13,18.33,50,18.33,50s18.33-8,18.33-18.15V6.24A49.77,49.77,0,0,1,31.19,6Z"/><path class="cls-2" d="M22.86,16.54,31.19,6c-11-1.22-12.86-6-12.86-6S15.85,6.24,0,6.24V31.85c0,3.1,1.72,6,4.1,8.53l6.15-7.82Z"/><path class="cls-3" d="M25.6,24.46H24.53V20.91a6.7,6.7,0,0,0-1.67-4.45h0l-.11-.13a6,6,0,0,0-8.84,0,6.7,6.7,0,0,0-1.78,4.58v3.55H11.06a.8.8,0,0,0-.8.8v7.22h0v2.17a.8.8,0,0,0,.8.8H25.6a.8.8,0,0,0,.8-.8V25.26A.8.8,0,0,0,25.6,24.46Zm-4,0H15.07V20.91a3.75,3.75,0,0,1,1-2.57,3,3,0,0,1,4.54,0,3.67,3.67,0,0,1,.39.5h0a3.8,3.8,0,0,1,.6,2.06Z"/></svg>
|
||||
|
After Width: | Height: | Size: 894 B |
138
src/sql/parts/admin/common/adminService.ts
Normal file
138
src/sql/parts/admin/common/adminService.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export const SERVICE_ID = 'adminService';
|
||||
|
||||
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { CreateLoginInput } from 'sql/parts/admin/security/createLoginInput';
|
||||
import { TaskDialogInput } from 'sql/parts/tasks/dialog/taskDialogInput';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
|
||||
import data = require('data');
|
||||
|
||||
export const IAdminService = createDecorator<IAdminService>(SERVICE_ID);
|
||||
|
||||
export interface IAdminService {
|
||||
_serviceBrand: any;
|
||||
|
||||
registerProvider(providerId: string, provider: data.AdminServicesProvider): void;
|
||||
|
||||
showCreateDatabaseWizard(uri: string, connection: IConnectionProfile): Promise<any>;
|
||||
|
||||
showCreateLoginWizard(uri: string, connection: IConnectionProfile): Promise<any>;
|
||||
|
||||
createDatabase(connectionUri: string, database: data.DatabaseInfo): Thenable<data.CreateDatabaseResponse>;
|
||||
|
||||
getDefaultDatabaseInfo(connectionUri: string): Thenable<data.DatabaseInfo>;
|
||||
|
||||
getDatabaseInfo(connectionUri: string): Thenable<data.DatabaseInfo>;
|
||||
}
|
||||
|
||||
export class AdminService implements IAdminService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private _providers: { [handle: string]: data.AdminServicesProvider; } = Object.create(null);
|
||||
|
||||
private _providerOptions: { [handle: string]: data.AdminServicesOptions; } = Object.create(null);
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
|
||||
@IConnectionManagementService private _connectionService: IConnectionManagementService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService
|
||||
) {
|
||||
if (_capabilitiesService && _capabilitiesService.onProviderRegisteredEvent) {
|
||||
_capabilitiesService.onProviderRegisteredEvent((capabilities => {
|
||||
this._providerOptions[capabilities.providerName] = capabilities.adminServicesProvider;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private _runAction<T>(uri: string, action: (handler: data.AdminServicesProvider) => Thenable<T>): Thenable<T> {
|
||||
let providerId: string = this._connectionService.getProviderIdFromUri(uri);
|
||||
|
||||
if (!providerId) {
|
||||
return TPromise.wrapError(new Error(localize('providerIdNotValidError', 'Connection is required in order to interact with adminservice')));
|
||||
}
|
||||
let handler = this._providers[providerId];
|
||||
if (handler) {
|
||||
return action(handler);
|
||||
} else {
|
||||
return TPromise.wrapError(new Error(localize('noHandlerRegistered', 'No Handler Registered')));
|
||||
}
|
||||
}
|
||||
|
||||
public showCreateDatabaseWizard(uri: string, connection: IConnectionProfile): Promise<any> {
|
||||
const self = this;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
let input: TaskDialogInput = self._instantiationService ? self._instantiationService.createInstance(TaskDialogInput, uri, connection) : undefined;
|
||||
self._editorService.openEditor(input, { pinned: true }, false);
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
public createDatabase(connectionUri: string, database: data.DatabaseInfo): Thenable<data.CreateDatabaseResponse> {
|
||||
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
|
||||
if (providerId) {
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
return provider.createDatabase(connectionUri, database);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public showCreateLoginWizard(uri: string, connection: IConnectionProfile): Promise<any> {
|
||||
const self = this;
|
||||
self.createLogin(uri, { name: 'TEST: login name' });
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
let loginInput: CreateLoginInput = self._instantiationService ? self._instantiationService.createInstance(CreateLoginInput, uri, connection) : undefined;
|
||||
self._editorService.openEditor(loginInput, { pinned: true }, false);
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
public createLogin(connectionUri: string, login: data.LoginInfo): Thenable<data.CreateLoginResponse> {
|
||||
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
|
||||
if (providerId) {
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
return provider.createLogin(connectionUri, login);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public getDefaultDatabaseInfo(connectionUri: string): Thenable<data.DatabaseInfo> {
|
||||
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
|
||||
if (providerId) {
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
return provider.getDefaultDatabaseInfo(connectionUri);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public getDatabaseInfo(connectionUri: string): Thenable<data.DatabaseInfo> {
|
||||
return this._runAction(connectionUri, (runner) => {
|
||||
return runner.getDatabaseInfo(connectionUri);
|
||||
});
|
||||
}
|
||||
|
||||
public registerProvider(providerId: string, provider: data.AdminServicesProvider): void {
|
||||
this._providers[providerId] = provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
|
||||
<div class="task-content">
|
||||
|
||||
<form #f="ngForm"
|
||||
(ngSubmit)="onSubmit(f)">
|
||||
|
||||
<span>General</span>
|
||||
|
||||
<table class="task-input-table">
|
||||
<tr>
|
||||
<td class="task-input-table-label-column">
|
||||
Database name:
|
||||
</td>
|
||||
<td class="task-input-table-content-column">
|
||||
<input name="databaseName" type="text" id="databaseNameInput" class="task-text-input"
|
||||
ngModel required [disabled]="formSubmitted" #databaseName="ngModel" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--
|
||||
<tr>
|
||||
<td class="task-input-table-label-column">
|
||||
Owner:
|
||||
</td>
|
||||
<td class="task-input-table-content-column">
|
||||
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<td style="width:100%">
|
||||
<input name="databaseOwner" type="text" id="databaseOwnerInput" class="task-text-input"
|
||||
ngModel required #databaseOwner="ngModel" />
|
||||
</td>
|
||||
<td style="width:auto; padding-left: 5px;">
|
||||
<button (click)="onSelectOwner()">...</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<label><input id="full-text-index" type="checkbox" /><span>Use full-text indexing</span></label>
|
||||
</td>
|
||||
</tr>
|
||||
-->
|
||||
|
||||
</table>
|
||||
|
||||
<span>{{databaseFilesLabel}}</span>
|
||||
<table class="task-input-table">
|
||||
<tr>
|
||||
<td>
|
||||
<p-dataTable [value]="databaseFiles" [tableStyle]="{'overflow': 'auto'}" [emptyMessage]="noRecordsFoundLabel">
|
||||
<p-column header="Logical Name" field="logicalName" ></p-column>
|
||||
<p-column header="File Type" field="fileType"></p-column>
|
||||
<p-column header="File Group" field="filegroup"></p-column>
|
||||
<p-column header="Initial Size" field="initialSize"></p-column>
|
||||
<p-column header="Autogrow" field="autogrow"></p-column>
|
||||
<p-column header="Path" field="path"></p-column>
|
||||
</p-dataTable>
|
||||
</td>
|
||||
</tr>
|
||||
<!--
|
||||
<tr>
|
||||
<td class="task-button-bar" style="padding-top: 5px">
|
||||
<button>Add</button>
|
||||
<button>Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
-->
|
||||
</table>
|
||||
|
||||
<!--
|
||||
<span>Options</span>
|
||||
<table class="task-input-table">
|
||||
<tr>
|
||||
<td class="task-input-table-label-column">
|
||||
Collation:
|
||||
</td>
|
||||
<td class="task-input-table-content-column">
|
||||
<select>
|
||||
<option>default</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="task-input-table-label-column">
|
||||
Recovery model:
|
||||
</td>
|
||||
<td class="task-input-table-content-column">
|
||||
<select>
|
||||
<option>Full</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="task-input-table-label-column">
|
||||
Compatability level:
|
||||
</td>
|
||||
<td class="task-input-table-content-column">
|
||||
<select>
|
||||
<option>SQL Server vNext (140)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="task-input-table-label-column">
|
||||
Containment type:
|
||||
</td>
|
||||
<td class="task-input-table-content-column">
|
||||
<select>
|
||||
<option>None</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
-->
|
||||
|
||||
<div class="task-button-bar">
|
||||
<button type="submit" id="ok-button" [disabled]="formSubmitted">OK</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChangeDetectorRef, ElementRef, Component, forwardRef, Inject } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { TaskDialogComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
|
||||
import { IAdminService } from 'sql/parts/admin/common/adminService';
|
||||
import { ITaskDialogComponent } from 'sql/parts/tasks/common/tasks';
|
||||
|
||||
import data = require('data');
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export const CREATEDATABASE_SELECTOR: string = 'createdatabase-component';
|
||||
|
||||
export interface DatabaseFile {
|
||||
logicalName: string;
|
||||
fileType: string;
|
||||
filegroup: string;
|
||||
initialSize: string;
|
||||
autogrow: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: CREATEDATABASE_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/admin/database/create/createDatabase.component.html'))
|
||||
})
|
||||
export class CreateDatabaseComponent implements ITaskDialogComponent {
|
||||
|
||||
private _adminService: IAdminService;
|
||||
|
||||
public formSubmitted: boolean = false;
|
||||
|
||||
public ownerUri: string;
|
||||
|
||||
public connection: ConnectionManagementInfo;
|
||||
|
||||
public databaseFiles: DatabaseFile[] = [];
|
||||
|
||||
// tslint:disable:no-unused-variable
|
||||
private readonly databaseFilesLabel: string = nls.localize('createDatabase.databaseFiles', 'Database files:');
|
||||
private readonly noRecordsFoundLabel: string = nls.localize('createDatabase.noRecordsFound', 'No records found');
|
||||
// tslint:enable:no-unused-variable
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService
|
||||
) {
|
||||
this._adminService = this._bootstrapService.adminService;
|
||||
}
|
||||
|
||||
private getDatabaseInfo(form: NgForm): data.DatabaseInfo {
|
||||
return <data.DatabaseInfo>{
|
||||
options: {
|
||||
name: form.value.databaseName,
|
||||
owner: form.value.databaseOwner
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public onSubmit(form: NgForm): void {
|
||||
this._adminService.createDatabase(this.ownerUri, this.getDatabaseInfo(form));
|
||||
this.formSubmitted = true;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
public onOk(): void { }
|
||||
|
||||
public onGenerateScript(): void { }
|
||||
|
||||
public onCancel(): void { }
|
||||
|
||||
public onSelectOwner(): void { }
|
||||
|
||||
public injectBootstapper(parameters: TaskDialogComponentParams ): void {
|
||||
let self = this;
|
||||
this.ownerUri = parameters.ownerUri;
|
||||
this._adminService.getDefaultDatabaseInfo(this.ownerUri).then(dbInfo => {
|
||||
let databaseFilesCount = dbInfo.options['databaseFilesCount'];
|
||||
for (let i = 0; i < databaseFilesCount; ++i) {
|
||||
self.databaseFiles[i] = {
|
||||
logicalName: dbInfo.options['databaseFiles.' + i + '.name'],
|
||||
fileType: dbInfo.options['databaseFiles.' + i + '.databaseFileType'],
|
||||
filegroup: dbInfo.options['databaseFiles.' + i + '.fileGroup'],
|
||||
initialSize: dbInfo.options['databaseFiles.' + i + '.initialSize'],
|
||||
autogrow: dbInfo.options['databaseFiles.' + i + '.autogrowth'],
|
||||
path: dbInfo.options['databaseFiles.' + i + '.folder']
|
||||
};
|
||||
}
|
||||
self._changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
}
|
||||
34
src/sql/parts/admin/security/createLogin.component.html
Normal file
34
src/sql/parts/admin/security/createLogin.component.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
|
||||
<h1>New Login</h1>
|
||||
|
||||
<h2>General</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
Login name:
|
||||
</td>
|
||||
<td>
|
||||
<input type="textbox" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
Password:
|
||||
</td>
|
||||
<td>
|
||||
<input type="textbox" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<button type="button">Cancel</button>
|
||||
<button type="button">Create Login</button>
|
||||
30
src/sql/parts/admin/security/createLogin.component.ts
Normal file
30
src/sql/parts/admin/security/createLogin.component.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ElementRef, Component, Inject, forwardRef } from '@angular/core';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { DashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
|
||||
|
||||
export const CREATELOGIN_SELECTOR: string = 'createlogin-component';
|
||||
|
||||
@Component({
|
||||
selector: CREATELOGIN_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/admin/security/createLogin.component.html'))
|
||||
})
|
||||
export class CreateLoginComponent {
|
||||
|
||||
public ownerUri: string;
|
||||
|
||||
public connection: ConnectionManagementInfo;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService
|
||||
) {
|
||||
let parameters: DashboardComponentParams = this._bootstrapService.getBootstrapParams(this._el.nativeElement.tagName);
|
||||
|
||||
}
|
||||
}
|
||||
39
src/sql/parts/admin/security/createLogin.module.ts
Normal file
39
src/sql/parts/admin/security/createLogin.module.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { NgModule, Inject, forwardRef, ApplicationRef, ComponentFactoryResolver } from '@angular/core';
|
||||
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
|
||||
import { CreateLoginComponent, CREATELOGIN_SELECTOR } from 'sql/parts/admin/security/createLogin.component';
|
||||
|
||||
// Connection Dashboard main angular module
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CreateLoginComponent
|
||||
],
|
||||
entryComponents: [CreateLoginComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BrowserModule
|
||||
],
|
||||
providers: [{ provide: APP_BASE_HREF, useValue: '/' }]
|
||||
})
|
||||
export class CreateLoginModule {
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService
|
||||
) {
|
||||
}
|
||||
|
||||
ngDoBootstrap(appRef: ApplicationRef) {
|
||||
const factory = this._resolver.resolveComponentFactory(CreateLoginComponent);
|
||||
const uniqueSelector: string = this._bootstrapService.getUniqueSelector(CREATELOGIN_SELECTOR);
|
||||
(<any>factory).factory.selector = uniqueSelector;
|
||||
appRef.bootstrap(factory);
|
||||
}
|
||||
}
|
||||
114
src/sql/parts/admin/security/createLoginEditor.ts
Normal file
114
src/sql/parts/admin/security/createLoginEditor.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/parts/query/editor/media/queryEditor';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Dimension, Builder } from 'vs/base/browser/builder';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CreateLoginInput } from './createLoginInput';
|
||||
import { CreateLoginModule } from './createLogin.module';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { IMetadataService } from 'sql/services/metadata/metadataService';
|
||||
import { IScriptingService } from 'sql/services/scripting/scriptingService';
|
||||
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
|
||||
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { DashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { CREATELOGIN_SELECTOR } from 'sql/parts/admin/security/createLogin.component';
|
||||
|
||||
export class CreateLoginEditor extends BaseEditor {
|
||||
|
||||
public static ID: string = 'workbench.editor.createlogin';
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IConnectionManagementService private _connectionService: IConnectionManagementService,
|
||||
@IMetadataService private _metadataService: IMetadataService,
|
||||
@IScriptingService private _scriptingService: IScriptingService,
|
||||
@IQueryEditorService private _queryEditorService: IQueryEditorService,
|
||||
@IBootstrapService private _bootstrapService: IBootstrapService
|
||||
) {
|
||||
super(CreateLoginEditor.ID, telemetryService, themeService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to create the editor in the parent builder.
|
||||
*/
|
||||
public createEditor(parent: Builder): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
|
||||
*/
|
||||
public focus(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
|
||||
* To be called when the container of this editor changes size.
|
||||
*/
|
||||
public layout(dimension: Dimension): void {
|
||||
}
|
||||
|
||||
public setInput(input: CreateLoginInput, options: EditorOptions): TPromise<void> {
|
||||
if (this.input instanceof CreateLoginInput && this.input.matches(input)) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
if (!input.hasInitialized) {
|
||||
this.bootstrapAngular(input);
|
||||
}
|
||||
this.revealElementWithTagName(input.uniqueSelector, this.getContainer().getHTMLElement());
|
||||
|
||||
return super.setInput(input, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveal the child element with the given tagName and hide all other elements.
|
||||
*/
|
||||
private revealElementWithTagName(tagName: string, parent: HTMLElement): void {
|
||||
let elementToReveal: HTMLElement;
|
||||
|
||||
for (let i = 0; i < parent.children.length; i++) {
|
||||
let child: HTMLElement = <HTMLElement>parent.children[i];
|
||||
if (child.tagName && child.tagName.toLowerCase() === tagName && !elementToReveal) {
|
||||
elementToReveal = child;
|
||||
} else {
|
||||
child.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (elementToReveal) {
|
||||
elementToReveal.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the angular components and record for this input that we have done so
|
||||
*/
|
||||
private bootstrapAngular(input: CreateLoginInput): void {
|
||||
|
||||
// Get the bootstrap params and perform the bootstrap
|
||||
let params: DashboardComponentParams = {
|
||||
connection: input.getConnectionProfile(),
|
||||
ownerUri: input.getUri()
|
||||
};
|
||||
let uniqueSelector = this._bootstrapService.bootstrap(
|
||||
CreateLoginModule,
|
||||
this.getContainer().getHTMLElement(),
|
||||
CREATELOGIN_SELECTOR,
|
||||
params);
|
||||
input.setUniqueSelector(uniqueSelector);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
57
src/sql/parts/admin/security/createLoginInput.ts
Normal file
57
src/sql/parts/admin/security/createLoginInput.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { EditorInput, EditorModel } from 'vs/workbench/common/editor';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
|
||||
export class CreateLoginInput extends EditorInput {
|
||||
|
||||
public static ID: string = 'workbench.editorinputs.createlogininput';
|
||||
public static SCHEMA: string = 'adminlogincreate';
|
||||
|
||||
private _uniqueSelector: string;
|
||||
|
||||
constructor(private _uri: string, private _connection: IConnectionProfile) {
|
||||
super();
|
||||
}
|
||||
|
||||
public setUniqueSelector(uniqueSelector: string): void {
|
||||
this._uniqueSelector = uniqueSelector;
|
||||
}
|
||||
|
||||
public getTypeId(): string {
|
||||
return UntitledEditorInput.ID;
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
return this._connection.serverName + ':' + this._connection.databaseName;
|
||||
}
|
||||
|
||||
public getUri(): string {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public supportsSplitEditor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getConnectionProfile(): IConnectionProfile {
|
||||
return this._connection;
|
||||
}
|
||||
|
||||
public resolve(refresh?: boolean): TPromise<EditorModel> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get hasInitialized(): boolean {
|
||||
return !!this._uniqueSelector;
|
||||
}
|
||||
|
||||
public get uniqueSelector(): string {
|
||||
return this._uniqueSelector;
|
||||
}
|
||||
}
|
||||
167
src/sql/parts/common/customInputConverter.ts
Normal file
167
src/sql/parts/common/customInputConverter.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EditorInput } from 'vs/workbench/common/editor';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IEditorInput } from 'vs/platform/editor/common/editor';
|
||||
import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
|
||||
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
////// Exported public functions/vars
|
||||
|
||||
// prefix for untitled sql editors
|
||||
export const untitledFilePrefix = 'SQLQuery';
|
||||
|
||||
// mode identifier for SQL mode
|
||||
export const sqlModeId = 'sql';
|
||||
|
||||
/**
|
||||
* Checks if the specified input is supported by one our custom input types, and if so convert it
|
||||
* to that type.
|
||||
* @param input The input to check for conversion
|
||||
* @param options Editor options for controlling the conversion
|
||||
* @param instantiationService The instatianation service to use to create the new input types
|
||||
*/
|
||||
export function convertEditorInput(input: EditorInput, options: IQueryEditorOptions, instantiationService: IInstantiationService): EditorInput {
|
||||
let denyQueryEditor = options && options.denyQueryEditor;
|
||||
if (input && !denyQueryEditor) {
|
||||
//QueryInput
|
||||
let uri: URI = getQueryEditorFileUri(input);
|
||||
if (uri) {
|
||||
const queryResultsInput: QueryResultsInput = instantiationService.createInstance(QueryResultsInput, uri.toString());
|
||||
let queryInput: QueryInput = instantiationService.createInstance(QueryInput, input.getName(), '', input, queryResultsInput, undefined);
|
||||
return queryInput;
|
||||
}
|
||||
|
||||
//QueryPlanInput
|
||||
uri = getQueryPlanEditorUri(input);
|
||||
if(uri) {
|
||||
let queryPlanXml: string = fs.readFileSync(uri.fsPath);
|
||||
let queryPlanInput: QueryPlanInput = instantiationService.createInstance(QueryPlanInput, queryPlanXml, 'aaa', undefined);
|
||||
return queryPlanInput;
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resource of the input if it's one of the ones we support.
|
||||
* @param input The IEditorInput to get the resource of
|
||||
*/
|
||||
export function getSupportedInputResource(input: IEditorInput): URI {
|
||||
if (input instanceof UntitledEditorInput) {
|
||||
let untitledCast: UntitledEditorInput = <UntitledEditorInput> input;
|
||||
if (untitledCast) {
|
||||
return untitledCast.getResource();
|
||||
}
|
||||
}
|
||||
|
||||
if (input instanceof FileEditorInput) {
|
||||
let fileCast: FileEditorInput = <FileEditorInput> input;
|
||||
if (fileCast) {
|
||||
return fileCast.getResource();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
////// Non-Exported Private functions/vars
|
||||
|
||||
// file extensions for the inputs we support (should be all upper case for comparison)
|
||||
const sqlFileTypes = ['SQL'];
|
||||
const sqlPlanFileTypes = ['SQLPLAN'];
|
||||
|
||||
/**
|
||||
* If input is a supported query editor file, return it's URI. Otherwise return undefined.
|
||||
* @param input The EditorInput to retrieve the URI of
|
||||
*/
|
||||
function getQueryEditorFileUri(input: EditorInput): URI {
|
||||
if (!input || !input.getName()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If this editor is not already of type queryinput
|
||||
if (!(input instanceof QueryInput)) {
|
||||
|
||||
// If this editor has a URI
|
||||
let uri: URI = getSupportedInputResource(input);
|
||||
if (uri) {
|
||||
let isValidUri: boolean = !!uri && !!uri.toString;
|
||||
|
||||
if (isValidUri && (hasFileExtension(sqlFileTypes, input, true) || hasSqlFileMode(input)) ) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If input is a supported query plan editor file (.sqlplan), return it's URI. Otherwise return undefined.
|
||||
* @param input The EditorInput to get the URI of
|
||||
*/
|
||||
function getQueryPlanEditorUri(input: EditorInput): URI {
|
||||
if (!input || !input.getName()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If this editor is not already of type queryinput
|
||||
if (!(input instanceof QueryPlanInput)) {
|
||||
let uri: URI = getSupportedInputResource(input);
|
||||
if(uri) {
|
||||
if (hasFileExtension(sqlPlanFileTypes, input, false)) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given EditorInput is set to either undefined or sql mode
|
||||
* @param input The EditorInput to check the mode of
|
||||
*/
|
||||
function hasSqlFileMode(input: EditorInput): boolean {
|
||||
if (input instanceof UntitledEditorInput) {
|
||||
let untitledCast: UntitledEditorInput = <UntitledEditorInput> input;
|
||||
return untitledCast && (untitledCast.getModeId() === undefined || untitledCast.getModeId() === sqlModeId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the name of the specified input has an extension that is
|
||||
* @param extensions The extensions to check for
|
||||
* @param input The input to check for the specified extensions
|
||||
*/
|
||||
function hasFileExtension(extensions: string[], input: EditorInput, checkUntitledFileType: boolean): boolean {
|
||||
// Check the extension type
|
||||
let lastPeriodIndex = input.getName().lastIndexOf('.');
|
||||
if (lastPeriodIndex > -1) {
|
||||
let extension: string = input.getName().substr(lastPeriodIndex + 1).toUpperCase();
|
||||
return !!extensions.find(x => x === extension);
|
||||
}
|
||||
|
||||
// Check for untitled file type
|
||||
if (checkUntitledFileType && input.getName().includes(untitledFilePrefix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return false if not a queryEditor file
|
||||
return false;
|
||||
}
|
||||
|
||||
16
src/sql/parts/common/rxjsUtils.ts
Normal file
16
src/sql/parts/common/rxjsUtils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Subscription } from 'rxjs/Subscription';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export function toDisposableSubscription(sub: Subscription): IDisposable {
|
||||
return {
|
||||
dispose: () => {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
89
src/sql/parts/connection/common/connection.contribution.ts
Normal file
89
src/sql/parts/connection/common/connection.contribution.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionGalleryService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/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';
|
||||
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 { 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 { 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 { 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);
|
||||
registerSingleton(IExtensionTipsService, ExtensionTipsService);
|
||||
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
|
||||
|
||||
// Connection Dashboard registration
|
||||
const dashboardEditorDescriptor = new EditorDescriptor(
|
||||
DashboardEditor.ID,
|
||||
'Dashboard',
|
||||
'sql/parts/dashboard/dashboardEditor',
|
||||
'DashboardEditor'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(dashboardEditorDescriptor, [new SyncDescriptor(DashboardInput)]);
|
||||
|
||||
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
|
||||
|
||||
// Connection Actions
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
ClearRecentConnectionsAction,
|
||||
ClearRecentConnectionsAction.ID,
|
||||
ClearRecentConnectionsAction.LABEL
|
||||
),
|
||||
ClearRecentConnectionsAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
AddServerGroupAction,
|
||||
AddServerGroupAction.ID,
|
||||
AddServerGroupAction.LABEL
|
||||
),
|
||||
AddServerGroupAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
AddServerAction,
|
||||
AddServerAction.ID,
|
||||
AddServerAction.LABEL
|
||||
),
|
||||
AddServerAction.LABEL
|
||||
);
|
||||
|
||||
let configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'connection',
|
||||
'title': 'Connection',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'sql.maxRecentConnections': {
|
||||
'type': 'number',
|
||||
'default': 25,
|
||||
'description': localize('sql.maxRecentConnectionsDescription', 'The maximum number of recently used connections to store in the connection list.')
|
||||
},
|
||||
'sql.defaultEngine': {
|
||||
'type': 'string',
|
||||
'description': localize('sql.defaultEngineDescription', 'Default SQL Engine to use. This drives default language provider in .sql files and the default to use when creating a new connection.'),
|
||||
'default': 'MSSQL'
|
||||
},
|
||||
}
|
||||
});
|
||||
27
src/sql/parts/connection/common/connection.ts
Normal file
27
src/sql/parts/connection/common/connection.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// ------------------------------- < Cancel Connect Request > ---------------------------------------
|
||||
|
||||
/**
|
||||
* Cancel connect request message format
|
||||
*/
|
||||
export class CancelConnectParams {
|
||||
/**
|
||||
* URI identifying the owner of the connection
|
||||
*/
|
||||
public ownerUri: string;
|
||||
}
|
||||
|
||||
// ------------------------------- </ Cancel Connect Request > --------------------------------------
|
||||
|
||||
// ------------------------------- < Disconnect Request > -------------------------------------------
|
||||
|
||||
// Disconnect request message format
|
||||
export class DisconnectParams {
|
||||
// URI identifying the owner of the connection
|
||||
public ownerUri: string;
|
||||
}
|
||||
|
||||
// ------------------------------- </ Disconnect Request > ------------------------------------------
|
||||
59
src/sql/parts/connection/common/connectionActions.ts
Normal file
59
src/sql/parts/connection/common/connectionActions.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { 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 { IMessageService, Severity } from 'vs/platform/message/common/message';
|
||||
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
|
||||
|
||||
/**
|
||||
* Locates the active editor and calls runQuery() on the editor if it is a QueryEditor.
|
||||
*/
|
||||
export class ClearRecentConnectionsAction extends Action {
|
||||
|
||||
public static ID = 'clearRecentConnectionsAction';
|
||||
public static LABEL = nls.localize('ClearRecentlyUsedLabel', 'Clear Recent Connections List');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
543
src/sql/parts/connection/common/connectionConfig.ts
Normal file
543
src/sql/parts/connection/common/connectionConfig.ts
Normal file
@@ -0,0 +1,543 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as Constants from './constants';
|
||||
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 { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { IConfigurationValue as TConfigurationValue } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConnectionProfile } from './connectionProfile';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import * as data from 'data';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
|
||||
export interface ISaveGroupResult {
|
||||
groups: IConnectionProfileGroup[];
|
||||
newGroupId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements connection profile file storage.
|
||||
*/
|
||||
export class ConnectionConfig implements IConnectionConfig {
|
||||
|
||||
private _providerCapabilitiesMap: { [providerName: string]: data.DataProtocolServerCapabilities };
|
||||
private _providerCachedCapabilitiesMap: { [providerName: string]: data.DataProtocolServerCapabilities };
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public constructor(
|
||||
private _configurationEditService: IConfigurationEditingService,
|
||||
private _workspaceConfigurationService: IWorkspaceConfigurationService,
|
||||
private _capabilitiesService: ICapabilitiesService,
|
||||
private _cachedMetadata?: data.DataProtocolServerCapabilities[]
|
||||
) {
|
||||
this._providerCapabilitiesMap = {};
|
||||
this._providerCachedCapabilitiesMap = {};
|
||||
}
|
||||
|
||||
public setCachedMetadata(cachedMetaData: data.DataProtocolServerCapabilities[]): void {
|
||||
this._cachedMetadata = cachedMetaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns connection groups from user and workspace settings.
|
||||
*/
|
||||
public getAllGroups(): IConnectionProfileGroup[] {
|
||||
|
||||
let allGroups: IConnectionProfileGroup[] = [];
|
||||
let userGroups = this.getConfiguration(Constants.connectionGroupsArrayName).user as IConnectionProfileGroup[];
|
||||
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);
|
||||
}
|
||||
allGroups = allGroups.concat(userGroups);
|
||||
}
|
||||
allGroups = allGroups.map(g => {
|
||||
if (g.parentId === '' || !g.parentId) {
|
||||
g.parentId = undefined;
|
||||
}
|
||||
return g;
|
||||
});
|
||||
return allGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
} else {
|
||||
let capabilities = this._capabilitiesService.getCapabilities();
|
||||
if (capabilities) {
|
||||
let providerCapabilities = capabilities.find(c => c.providerName === providerName);
|
||||
if (providerCapabilities) {
|
||||
this._providerCapabilitiesMap[providerName] = providerCapabilities;
|
||||
result = providerCapabilities;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 result;
|
||||
|
||||
}
|
||||
/**
|
||||
* Add a new connection to the connection config.
|
||||
*/
|
||||
public addConnection(profile: IConnectionProfile): Promise<IConnectionProfile> {
|
||||
return new Promise<IConnectionProfile>((resolve, reject) => {
|
||||
if (profile.saveProfile) {
|
||||
this.addGroupFromProfile(profile).then(groupId => {
|
||||
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
|
||||
if (!profiles) {
|
||||
profiles = [];
|
||||
}
|
||||
|
||||
let providerCapabilities = this.getCapabilities(profile.providerName);
|
||||
let connectionProfile = this.getConnectionProfileInstance(profile, groupId);
|
||||
let newProfile = ConnectionProfile.convertToProfileStore(providerCapabilities, connectionProfile);
|
||||
|
||||
// Remove the profile if already set
|
||||
var sameProfileInList = profiles.find(value => {
|
||||
let providerCapabilities = this.getCapabilities(value.providerName);
|
||||
let providerConnectionProfile = ConnectionProfile.createFromStoredProfile(value, providerCapabilities);
|
||||
return providerConnectionProfile.matches(connectionProfile);
|
||||
});
|
||||
if (sameProfileInList) {
|
||||
profiles = profiles.filter(value => value !== sameProfileInList);
|
||||
newProfile.id = sameProfileInList.id;
|
||||
connectionProfile.id = sameProfileInList.id;
|
||||
}
|
||||
|
||||
profiles.push(newProfile);
|
||||
|
||||
this.writeConfiguration(Constants.connectionsArrayName, profiles).then(() => {
|
||||
resolve(connectionProfile);
|
||||
}).catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getConnectionProfileInstance(profile: IConnectionProfile, groupId: string): ConnectionProfile {
|
||||
let connectionProfile = profile as ConnectionProfile;
|
||||
let providerCapabilities = this.getCapabilities(profile.providerName);
|
||||
if (connectionProfile === undefined) {
|
||||
connectionProfile = new ConnectionProfile(providerCapabilities, profile);
|
||||
}
|
||||
connectionProfile.groupId = groupId;
|
||||
return connectionProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
*Returns group id
|
||||
* @param groupName
|
||||
*/
|
||||
public addGroupFromProfile(profile: IConnectionProfile): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (profile.groupId && profile.groupId !== Utils.defaultGroupId) {
|
||||
resolve(profile.groupId);
|
||||
} else {
|
||||
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
|
||||
let result = this.saveGroup(groups, profile.groupFullName, undefined, undefined);
|
||||
groups = result.groups;
|
||||
|
||||
this.writeConfiguration(Constants.connectionGroupsArrayName, groups).then(() => {
|
||||
resolve(result.newGroupId);
|
||||
}).catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*Returns group id
|
||||
* @param groupName
|
||||
*/
|
||||
public addGroup(profileGroup: IConnectionProfileGroup): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (profileGroup.id) {
|
||||
resolve(profileGroup.id);
|
||||
} else {
|
||||
let groups = this._workspaceConfigurationService.lookup<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.");
|
||||
reject(errMessage);
|
||||
} else {
|
||||
let result = this.saveGroup(groups, profileGroup.name, profileGroup.color, profileGroup.description);
|
||||
groups = result.groups;
|
||||
|
||||
this.writeConfiguration(Constants.connectionGroupsArrayName, groups).then(() => {
|
||||
resolve(result.newGroupId);
|
||||
}).catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getConnectionProfilesForTarget(configTarget: ConfigurationTarget): IConnectionProfileStore[] {
|
||||
let configs = this.getConfiguration(Constants.connectionsArrayName);
|
||||
let profiles: IConnectionProfileStore[];
|
||||
if (configs) {
|
||||
if (configTarget === ConfigurationTarget.USER) {
|
||||
profiles = <IConnectionProfileStore[]>configs.user;
|
||||
} else if (configTarget === ConfigurationTarget.WORKSPACE) {
|
||||
profiles = <IConnectionProfileStore[]>configs.workspace;
|
||||
}
|
||||
if (profiles) {
|
||||
if(this.fixConnectionIds(profiles)) {
|
||||
this.writeConfiguration(Constants.connectionsArrayName, profiles, configTarget);
|
||||
}
|
||||
} else {
|
||||
profiles = [];
|
||||
}
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace duplicate ids with new ones. Sets id for the profiles without id
|
||||
* @param profiles
|
||||
*/
|
||||
public fixConnectionIds(profiles: IConnectionProfileStore[]): boolean {
|
||||
let idsCache: { [label: string]: boolean } = {};
|
||||
let changed: boolean = false;
|
||||
for (var index = 0; index < profiles.length; index++) {
|
||||
var profile = profiles[index];
|
||||
if (!profile.id) {
|
||||
profile.id = generateUuid();
|
||||
changed = true;
|
||||
}
|
||||
if (profile.id in idsCache) {
|
||||
profile.id = generateUuid();
|
||||
changed = true;
|
||||
}
|
||||
idsCache[profile.id] = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all connections in the connection config. Connections returned
|
||||
* are sorted first by whether they were found in the user/workspace settings,
|
||||
* and next alphabetically by profile/server name.
|
||||
*/
|
||||
public getConnections(getWorkspaceConnections: boolean): ConnectionProfile[] {
|
||||
let profiles: IConnectionProfileStore[] = [];
|
||||
//TODO: have to figure out how to sort connections for all provider
|
||||
// Read from user settings
|
||||
|
||||
let userProfiles: IConnectionProfileStore[] = this.getConnectionProfilesForTarget(ConfigurationTarget.USER);
|
||||
if (userProfiles !== undefined) {
|
||||
profiles = profiles.concat(userProfiles);
|
||||
}
|
||||
|
||||
if (getWorkspaceConnections) {
|
||||
// Read from workspace settings
|
||||
|
||||
let workspaceProfiles: IConnectionProfileStore[] = this.getConnectionProfilesForTarget(ConfigurationTarget.WORKSPACE);
|
||||
if (workspaceProfiles !== undefined) {
|
||||
profiles = profiles.concat(workspaceProfiles);
|
||||
}
|
||||
}
|
||||
|
||||
let connectionProfiles = profiles.map(p => {
|
||||
let capabilitiesForProvider = this.getCapabilities(p.providerName);
|
||||
|
||||
let providerConnectionProfile = ConnectionProfile.createFromStoredProfile(p, capabilitiesForProvider);
|
||||
providerConnectionProfile.setServerCapabilities(capabilitiesForProvider);
|
||||
this._capabilitiesService.onProviderRegisteredEvent((serverCapabilities) => {
|
||||
providerConnectionProfile.onProviderRegistered(serverCapabilities);
|
||||
});
|
||||
|
||||
return providerConnectionProfile;
|
||||
});
|
||||
|
||||
return connectionProfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a connection profile from settings.
|
||||
*/
|
||||
public deleteConnection(profile: ConnectionProfile): Promise<void> {
|
||||
// Get all connections in the settings
|
||||
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
|
||||
// Remove the profile from the connections
|
||||
profiles = profiles.filter(value => {
|
||||
let providerCapabilities = this.getCapabilities(value.providerName);
|
||||
let providerConnectionProfile = ConnectionProfile.createFromStoredProfile(value, providerCapabilities);
|
||||
return providerConnectionProfile.getOptionsKey() !== profile.getOptionsKey();
|
||||
});
|
||||
|
||||
// Write connections back to settings
|
||||
return this.writeConfiguration(Constants.connectionsArrayName, profiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a group and all its child connections and groups from settings.
|
||||
* Fails if writing to settings fails.
|
||||
*/
|
||||
public deleteGroup(group: ConnectionProfileGroup): Promise<void> {
|
||||
let connections = ConnectionProfileGroup.getConnectionsInGroup(group);
|
||||
let subgroups = ConnectionProfileGroup.getSubgroups(group);
|
||||
// Add selected group to subgroups list
|
||||
subgroups.push(group);
|
||||
// Get all connections in the settings
|
||||
let profiles = this._workspaceConfigurationService.lookup<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
|
||||
// Remove the profiles from the connections
|
||||
profiles = profiles.filter(value => {
|
||||
let providerCapabilities = this.getCapabilities(value.providerName);
|
||||
let providerConnectionProfile = ConnectionProfile.createFromStoredProfile(value, providerCapabilities);
|
||||
return !connections.some((val) => val.getOptionsKey() === providerConnectionProfile.getOptionsKey());
|
||||
});
|
||||
|
||||
// Get all groups in the settings
|
||||
let groups = this._workspaceConfigurationService.lookup<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
|
||||
// Remove subgroups in the settings
|
||||
groups = groups.filter((grp) => {
|
||||
return !subgroups.some((item) => item.id === grp.id);
|
||||
});
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.writeConfiguration(Constants.connectionsArrayName, profiles).then(() => {
|
||||
this.writeConfiguration(Constants.connectionGroupsArrayName, groups).then(() => {
|
||||
resolve();
|
||||
}).catch(() => reject());
|
||||
}).catch(() => reject());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
groups = groups.map(g => {
|
||||
if (g.id === source.id) {
|
||||
g.parentId = target.id;
|
||||
}
|
||||
return g;
|
||||
});
|
||||
return this.writeConfiguration(Constants.connectionGroupsArrayName, groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if connection can be moved to another group
|
||||
*/
|
||||
public canChangeConnectionConfig(profile: ConnectionProfile, newGroupID: string): boolean {
|
||||
let profiles = this.getConnections(true);
|
||||
let existingProfile = profiles.find(p => p.getConnectionInfoId() === profile.getConnectionInfoId()
|
||||
&& p.groupId === newGroupID);
|
||||
return existingProfile === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the connection under the target group with the new ID.
|
||||
*/
|
||||
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;
|
||||
if (profiles) {
|
||||
let providerCapabilities = this.getCapabilities(profile.providerName);
|
||||
if (profile.parent && profile.parent.id === Constants.unsavedGroupId) {
|
||||
profile.groupId = newGroupID;
|
||||
profiles.push(ConnectionProfile.convertToProfileStore(providerCapabilities, profile));
|
||||
} else {
|
||||
profiles.forEach((value) => {
|
||||
let configProf = ConnectionProfile.createFromStoredProfile(value, providerCapabilities);
|
||||
if (configProf.getOptionsKey() === profile.getOptionsKey()) {
|
||||
value.groupId = newGroupID;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.writeConfiguration(Constants.connectionsArrayName, profiles, target).then(result => {
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the connection under the target group with the new ID.
|
||||
*/
|
||||
public changeGroupIdForConnection(profile: ConnectionProfile, newGroupID: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!this.canChangeConnectionConfig(profile, newGroupID)) {
|
||||
// Same connection already exists in this group
|
||||
reject('Same connection already exists in the group');
|
||||
} else {
|
||||
this.changeGroupIdForConnectionInSettings(profile, newGroupID, ConfigurationTarget.USER).then(result1 => {
|
||||
this.changeGroupIdForConnectionInSettings(profile, newGroupID, ConfigurationTarget.WORKSPACE).then(result2 => {
|
||||
resolve();
|
||||
}).catch(error2 => {
|
||||
reject(error2);
|
||||
});
|
||||
}).catch(error1 => {
|
||||
reject(error1);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public saveGroup(groups: IConnectionProfileGroup[], groupFullName: string, color: string, description: string): ISaveGroupResult {
|
||||
let result: ISaveGroupResult;
|
||||
let groupNames = ConnectionProfileGroup.getGroupFullNameParts(groupFullName);
|
||||
result = this.saveGroupInTree(groups, undefined, groupNames, color, description, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public editGroup(source: ConnectionProfileGroup): Promise<void> {
|
||||
let groups = this._workspaceConfigurationService.lookup<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.");
|
||||
return Promise.reject(errMessage);
|
||||
}
|
||||
groups = groups.map(g => {
|
||||
if (g.id === source.id) {
|
||||
g.name = source.name;
|
||||
g.description = source.description;
|
||||
g.color = source.color;
|
||||
source.isRenamed = false;
|
||||
}
|
||||
return g;
|
||||
});
|
||||
return this.writeConfiguration(Constants.connectionGroupsArrayName, groups);
|
||||
}
|
||||
|
||||
private isSameGroupName(group1: IConnectionProfileGroup, group2: IConnectionProfileGroup): boolean {
|
||||
let sameGroupName: boolean = false;
|
||||
if (group1 && group2) {
|
||||
sameGroupName = ((!group1.name && !group2.name) || group1.name.toUpperCase() === group2.name.toUpperCase()) &&
|
||||
(group1.parentId === group2.parentId || (!group1.parentId && !group2.parentId));
|
||||
}
|
||||
return sameGroupName;
|
||||
}
|
||||
|
||||
private saveGroupInTree(groupTree: IConnectionProfileGroup[], parentId: string, groupNames: string[], color: string, description: string, index: number): ISaveGroupResult {
|
||||
if (!groupTree) {
|
||||
groupTree = [];
|
||||
}
|
||||
let newGroupId: string;
|
||||
|
||||
if (index < groupNames.length) {
|
||||
let groupName: string = groupNames[index];
|
||||
let newGroup: IConnectionProfileGroup = {
|
||||
name: groupName,
|
||||
id: undefined,
|
||||
parentId: parentId,
|
||||
color: color,
|
||||
description: description
|
||||
};
|
||||
let found = groupTree.find(group => this.isSameGroupName(group, newGroup));
|
||||
if (found) {
|
||||
if (index === groupNames.length - 1) {
|
||||
newGroupId = found.id;
|
||||
//Found the group full name
|
||||
} else {
|
||||
let result = this.saveGroupInTree(groupTree, found.id, groupNames, color, description, index + 1);
|
||||
groupTree = result.groups;
|
||||
newGroupId = result.newGroupId;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (ConnectionProfileGroup.isRoot(newGroup.name)) {
|
||||
newGroup.id = Utils.defaultGroupId;
|
||||
} else {
|
||||
newGroup.id = generateUuid();
|
||||
}
|
||||
let result = this.saveGroupInTree(groupTree, newGroup.id, groupNames, color, description, index + 1);
|
||||
newGroupId = result.newGroupId;
|
||||
groupTree = result.groups;
|
||||
groupTree.push(newGroup);
|
||||
if (index === groupNames.length - 1) {
|
||||
newGroupId = newGroup.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
let groupResult: ISaveGroupResult = {
|
||||
groups: groupTree,
|
||||
newGroupId: newGroupId
|
||||
};
|
||||
return groupResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all profiles from the parsed settings file.
|
||||
* This is public for testing only.
|
||||
* @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);
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace existing profiles in the settings file with a new set of profiles.
|
||||
* @param parsedSettingsFile an object representing the parsed contents of the settings file.
|
||||
* @param profiles the set of profiles to insert into the settings file.
|
||||
*/
|
||||
private writeConfiguration(
|
||||
key: string,
|
||||
profiles: IConnectionProfileStore[] | IConnectionProfileGroup[] | data.DataProtocolServerCapabilities[],
|
||||
target: ConfigurationTarget = ConfigurationTarget.USER): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let configValue: IConfigurationValue = {
|
||||
key: key,
|
||||
value: profiles
|
||||
};
|
||||
this._configurationEditService.writeConfiguration(target, configValue).then(result => {
|
||||
this._workspaceConfigurationService.reloadConfiguration().then(() => {
|
||||
resolve();
|
||||
});
|
||||
}, (error => {
|
||||
reject(error);
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
40
src/sql/parts/connection/common/connectionGlobalStatus.ts
Normal file
40
src/sql/parts/connection/common/connectionGlobalStatus.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { ConnectionSummary } from 'data';
|
||||
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
|
||||
import * as LocalizedConstants from 'sql/parts/connection/common/localizedConstants';
|
||||
|
||||
// Status when making connections from the viewlet
|
||||
export class ConnectionGlobalStatus {
|
||||
|
||||
private _displayTime: number = 5000; // (in ms)
|
||||
|
||||
constructor(
|
||||
@IStatusbarService private _statusBarService: IStatusbarService
|
||||
) {
|
||||
}
|
||||
|
||||
public setStatusToConnected(connectionSummary: ConnectionSummary): void {
|
||||
if (this._statusBarService) {
|
||||
let text: string;
|
||||
let connInfo: string = connectionSummary.serverName;
|
||||
if (connInfo) {
|
||||
if (connectionSummary.databaseName && connectionSummary.databaseName !== '') {
|
||||
connInfo = connInfo + ' : ' + connectionSummary.databaseName;
|
||||
} else {
|
||||
connInfo = connInfo + ' : ' + '<default>';
|
||||
}
|
||||
text = LocalizedConstants.onDidConnectMessage + ' ' + connInfo;
|
||||
}
|
||||
this._statusBarService.setStatusMessage(text, this._displayTime);
|
||||
}
|
||||
}
|
||||
|
||||
public setStatusToDisconnected(fileUri: string): void {
|
||||
if (this._statusBarService) {
|
||||
this._statusBarService.setStatusMessage(LocalizedConstants.onDidDisconnectMessage, this._displayTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/sql/parts/connection/common/connectionInfo.ts
Normal file
36
src/sql/parts/connection/common/connectionInfo.ts
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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import Interfaces = require('./interfaces');
|
||||
|
||||
/**
|
||||
* Sets sensible defaults for key connection properties, especially
|
||||
* if connection to Azure
|
||||
*
|
||||
* @export connectionInfo/fixupConnectionCredentials
|
||||
* @param {Interfaces.IConnectionCredentials} connCreds connection to be fixed up
|
||||
* @returns {Interfaces.IConnectionCredentials} the updated connection
|
||||
*/
|
||||
export function fixupConnectionCredentials(connCreds: Interfaces.IConnectionProfile): Interfaces.IConnectionProfile {
|
||||
if (!connCreds.serverName) {
|
||||
connCreds.serverName = '';
|
||||
}
|
||||
|
||||
if (!connCreds.databaseName) {
|
||||
connCreds.databaseName = '';
|
||||
}
|
||||
|
||||
if (!connCreds.userName) {
|
||||
connCreds.userName = '';
|
||||
}
|
||||
|
||||
if (!connCreds.password) {
|
||||
connCreds.password = '';
|
||||
}
|
||||
return connCreds;
|
||||
}
|
||||
|
||||
341
src/sql/parts/connection/common/connectionManagement.ts
Normal file
341
src/sql/parts/connection/common/connectionManagement.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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 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';
|
||||
|
||||
export interface IConnectionsViewlet extends IViewlet {
|
||||
search(text: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the actions that could happen after connecting is complete
|
||||
*/
|
||||
export interface IConnectionCompletionOptions {
|
||||
/**
|
||||
* save the connection to MRU and settings (only save to setting if profile.saveProfile is set to true)
|
||||
*/
|
||||
saveTheConnection: boolean;
|
||||
|
||||
/**
|
||||
* open the dashboard after connection is complete
|
||||
*/
|
||||
showDashboard: boolean;
|
||||
|
||||
/**
|
||||
* Parameters to be used if connecting from an editor
|
||||
*/
|
||||
params: INewConnectionParams;
|
||||
|
||||
/**
|
||||
* Open the connection dialog if connection fails
|
||||
*/
|
||||
showConnectionDialogOnError: boolean;
|
||||
|
||||
/**
|
||||
* Open the connection firewall rule dialog if connection fails
|
||||
*/
|
||||
showFirewallRuleOnError: boolean;
|
||||
}
|
||||
|
||||
export interface IConnectionResult {
|
||||
connected: boolean;
|
||||
errorMessage: string;
|
||||
errorCode: number;
|
||||
errorHandled?: boolean;
|
||||
}
|
||||
|
||||
export interface IConnectionCallbacks {
|
||||
onConnectStart(): void;
|
||||
onConnectReject(error?: string): void;
|
||||
onConnectSuccess(params?: INewConnectionParams): void;
|
||||
onDisconnect(): void;
|
||||
}
|
||||
|
||||
export const SERVICE_ID = 'connectionManagementService';
|
||||
|
||||
export const IConnectionManagementService = createDecorator<IConnectionManagementService>(SERVICE_ID);
|
||||
|
||||
export interface IConnectionManagementService {
|
||||
_serviceBrand: any;
|
||||
|
||||
// Event Emitters
|
||||
onAddConnectionProfile: Event<IConnectionProfile>;
|
||||
onDeleteConnectionProfile: Event<void>;
|
||||
onConnect: Event<IConnectionParams>;
|
||||
onDisconnect: Event<IConnectionParams>;
|
||||
onConnectionChanged: Event<IConnectionParams>;
|
||||
onLanguageFlavorChanged: Event<data.DidChangeLanguageFlavorParams>;
|
||||
|
||||
/**
|
||||
* Opens the connection dialog to create new connection
|
||||
*/
|
||||
showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, error?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Opens the add server group dialog
|
||||
*/
|
||||
showCreateServerGroupDialog(callbacks?: IServerGroupDialogCallbacks): Promise<void>;
|
||||
|
||||
/**
|
||||
* Opens the edit server group dialog
|
||||
*/
|
||||
showEditServerGroupDialog(group: ConnectionProfileGroup): Promise<void>;
|
||||
|
||||
/**
|
||||
* Load the password and opens a new connection
|
||||
*/
|
||||
connect(connection: IConnectionProfile, uri: string, options?: IConnectionCompletionOptions, callbacks?: IConnectionCallbacks): Promise<IConnectionResult>;
|
||||
|
||||
/**
|
||||
* Opens a new connection and save the profile in settings
|
||||
*/
|
||||
connectAndSaveProfile(connection: IConnectionProfile, uri: string, options?: IConnectionCompletionOptions, callbacks?: IConnectionCallbacks): Promise<IConnectionResult>;
|
||||
|
||||
/**
|
||||
* Finds existing connection for given profile and purpose is any exists.
|
||||
* The purpose is connection by default
|
||||
*/
|
||||
findExistingConnection(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): ConnectionProfile;
|
||||
|
||||
/**
|
||||
* If there's already a connection for given profile and purpose, returns the ownerUri for the connection
|
||||
* otherwise tries to make a connection and returns the owner uri when connection is complete
|
||||
* The purpose is connection by default
|
||||
*/
|
||||
connectIfNotConnected(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): Promise<string>;
|
||||
|
||||
/**
|
||||
* Adds the successful connection to MRU and send the connection error back to the connection handler for failed connections
|
||||
*/
|
||||
onConnectionComplete(handle: number, connectionInfoSummary: data.ConnectionInfoSummary): void;
|
||||
|
||||
onIntelliSenseCacheComplete(handle: number, connectionUri: string): void;
|
||||
|
||||
onConnectionChangedNotification(handle: number, changedConnInfo: data.ChangedConnectionInfo);
|
||||
|
||||
getConnectionGroups(): ConnectionProfileGroup[];
|
||||
|
||||
getRecentConnections(): ConnectionProfile[];
|
||||
|
||||
clearRecentConnectionsList(): void;
|
||||
|
||||
getActiveConnections(): ConnectionProfile[];
|
||||
|
||||
saveProfileGroup(profile: IConnectionProfileGroup): Promise<string>;
|
||||
|
||||
changeGroupIdForConnectionGroup(source: IConnectionProfileGroup, target: IConnectionProfileGroup): Promise<void>;
|
||||
|
||||
changeGroupIdForConnection(source: ConnectionProfile, targetGroupName: string): Promise<void>;
|
||||
|
||||
deleteConnection(connection: ConnectionProfile): Promise<boolean>;
|
||||
|
||||
deleteConnectionGroup(group: ConnectionProfileGroup): Promise<boolean>;
|
||||
|
||||
getAdvancedProperties(): data.ConnectionOption[];
|
||||
|
||||
getConnectionId(connectionProfile: IConnectionProfile): string;
|
||||
|
||||
getFormattedUri(uri: string, connectionProfile: IConnectionProfile): string;
|
||||
|
||||
isConnected(fileUri: string): boolean;
|
||||
|
||||
/**
|
||||
* Returns true if the connection profile is connected
|
||||
*/
|
||||
isProfileConnected(connectionProfile: IConnectionProfile): boolean;
|
||||
|
||||
/**
|
||||
* Returns true if the connection profile is connecting
|
||||
*/
|
||||
isProfileConnecting(connectionProfile: IConnectionProfile): boolean;
|
||||
|
||||
isRecent(connectionProfile: ConnectionProfile): boolean;
|
||||
|
||||
isConnected(fileUri: string, connectionProfile?: ConnectionProfile): boolean;
|
||||
|
||||
disconnectEditor(owner: IConnectableInput, force?: boolean): Promise<boolean>;
|
||||
|
||||
disconnect(connection: ConnectionProfile): Promise<void>;
|
||||
|
||||
disconnect(ownerUri: string): Promise<void>;
|
||||
|
||||
addSavedPassword(connectionProfile: IConnectionProfile): Promise<IConnectionProfile>;
|
||||
|
||||
listDatabases(connectionUri: string): Thenable<data.ListDatabasesResult>;
|
||||
|
||||
/**
|
||||
* Register a connection provider
|
||||
*/
|
||||
registerProvider(providerId: string, provider: data.ConnectionProvider): void;
|
||||
|
||||
editGroup(group: ConnectionProfileGroup): Promise<void>;
|
||||
|
||||
getConnectionProfile(fileUri: string): IConnectionProfile;
|
||||
|
||||
getConnectionInfo(fileUri: string): ConnectionManagementInfo;
|
||||
|
||||
/**
|
||||
* Cancels the connection
|
||||
*/
|
||||
cancelConnection(connection: IConnectionProfile): Thenable<boolean>;
|
||||
|
||||
/**
|
||||
* Changes the database for an active connection
|
||||
*/
|
||||
changeDatabase(connectionUri: string, databaseName: string): Thenable<boolean>;
|
||||
|
||||
/**
|
||||
* Cancels the connection for the editor
|
||||
*/
|
||||
cancelEditorConnection(owner: IConnectableInput): Thenable<boolean>;
|
||||
|
||||
showDashboard(connection: ConnectionProfile): Thenable<boolean>;
|
||||
|
||||
closeDashboard(uri: string): void;
|
||||
|
||||
getProviderIdFromUri(ownerUri: string): string;
|
||||
|
||||
hasRegisteredServers(): boolean;
|
||||
|
||||
getCapabilities(providerName: string): data.DataProtocolServerCapabilities;
|
||||
|
||||
canChangeConnectionConfig(profile: ConnectionProfile, newGroupID: string): boolean;
|
||||
|
||||
/**
|
||||
* Sends a notification that the language flavor for a given URI has changed.
|
||||
* For SQL, this would be the specific SQL implementation being used.
|
||||
*
|
||||
* @param {string} uri the URI of the resource whose language has changed
|
||||
* @param {string} language the base language
|
||||
* @param {string} flavor the specific language flavor that's been set
|
||||
*
|
||||
* @memberof IConnectionManagementService
|
||||
*/
|
||||
doChangeLanguageFlavor(uri: string, language: string, flavor: string): void;
|
||||
|
||||
/**
|
||||
* Ensures that a default language flavor is set for a URI, if none has already been defined.
|
||||
* @param {string} uri document identifier
|
||||
* @memberof ConnectionManagementService
|
||||
*/
|
||||
ensureDefaultLanguageFlavor(uri: string): void;
|
||||
|
||||
/**
|
||||
* Gets an array of all known providers.
|
||||
*
|
||||
* @returns {string[]} An array of provider names
|
||||
* @memberof IConnectionManagementService
|
||||
*/
|
||||
getProviderNames(): string[];
|
||||
|
||||
/**
|
||||
* Refresh the IntelliSense cache for the connection with the given URI
|
||||
*/
|
||||
rebuildIntelliSenseCache(uri: string): Thenable<void>;
|
||||
}
|
||||
|
||||
export const IConnectionDialogService = createDecorator<IConnectionDialogService>('connectionDialogService');
|
||||
export interface IConnectionDialogService {
|
||||
_serviceBrand: any;
|
||||
showDialog(connectionManagementService: IConnectionManagementService, params: INewConnectionParams, model: IConnectionProfile, error?: string): Thenable<void>;
|
||||
}
|
||||
|
||||
export interface IServerGroupDialogCallbacks {
|
||||
onAddGroup(groupName: string): void;
|
||||
onClose(): void;
|
||||
}
|
||||
export const IServerGroupController = createDecorator<IServerGroupController>('serverGroupController');
|
||||
export interface IServerGroupController {
|
||||
_serviceBrand: any;
|
||||
showCreateGroupDialog(connectionManagementService: IConnectionManagementService, callbacks?: IServerGroupDialogCallbacks): TPromise<void>;
|
||||
showEditGroupDialog(connectionManagementService: IConnectionManagementService, group: ConnectionProfileGroup): TPromise<void>;
|
||||
}
|
||||
|
||||
export const IErrorMessageService = createDecorator<IErrorMessageService>('errorMessageService');
|
||||
export interface IErrorMessageService {
|
||||
_serviceBrand: any;
|
||||
showDialog(severity: Severity, headerTitle: string, message: string): void;
|
||||
}
|
||||
|
||||
export enum ServiceOptionType {
|
||||
string = 0,
|
||||
multistring = 1,
|
||||
password = 2,
|
||||
number = 3,
|
||||
category = 4,
|
||||
boolean = 5
|
||||
}
|
||||
|
||||
export enum ConnectionOptionSpecialType {
|
||||
serverName = 0,
|
||||
databaseName = 1,
|
||||
authType = 2,
|
||||
userName = 3,
|
||||
password = 4,
|
||||
appName = 5
|
||||
}
|
||||
|
||||
export enum RunQueryOnConnectionMode {
|
||||
none = 0,
|
||||
executeQuery = 1,
|
||||
executeCurrentQuery = 2,
|
||||
estimatedQueryPlan = 3
|
||||
}
|
||||
|
||||
export interface INewConnectionParams {
|
||||
connectionType: ConnectionType;
|
||||
input?: IConnectableInput;
|
||||
runQueryOnCompletion?: RunQueryOnConnectionMode;
|
||||
querySelection?: ISelectionData;
|
||||
showDashboard?: boolean;
|
||||
}
|
||||
|
||||
export interface IConnectableInput {
|
||||
uri: string;
|
||||
onConnectStart(): void;
|
||||
onConnectReject(error?: string): void;
|
||||
onConnectSuccess(params?: INewConnectionParams): void;
|
||||
onDisconnect(): void;
|
||||
}
|
||||
|
||||
export enum ConnectionType {
|
||||
default = 0,
|
||||
editor = 1
|
||||
}
|
||||
|
||||
export enum MetadataType {
|
||||
Table = 0,
|
||||
View = 1,
|
||||
SProc = 2,
|
||||
Function = 3
|
||||
}
|
||||
|
||||
export enum TaskStatus {
|
||||
notStarted = 0,
|
||||
inProgress = 1,
|
||||
succeeded = 2,
|
||||
succeededWithWarning = 3,
|
||||
failed = 4,
|
||||
canceled = 5
|
||||
}
|
||||
|
||||
export interface IConnectionParams {
|
||||
connectionUri: string;
|
||||
connectionProfile: IConnectionProfile;
|
||||
}
|
||||
73
src/sql/parts/connection/common/connectionManagementInfo.ts
Normal file
73
src/sql/parts/connection/common/connectionManagementInfo.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
|
||||
import * as data from 'data';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
|
||||
/**
|
||||
* Information for a document's connection. Exported for testing purposes.
|
||||
*/
|
||||
export class ConnectionManagementInfo {
|
||||
/**
|
||||
* Connection GUID returned from the service host
|
||||
*/
|
||||
public connectionId: string;
|
||||
|
||||
|
||||
public providerId: string;
|
||||
|
||||
/**
|
||||
* Credentials used to connect
|
||||
*/
|
||||
public connectionProfile: ConnectionProfile;
|
||||
|
||||
/**
|
||||
* Callback for when a connection notification is received.
|
||||
*/
|
||||
public connectHandler: (result: boolean, errorMessage?: string, errorCode?: number) => void;
|
||||
|
||||
/**
|
||||
* Information about the SQL Server instance.
|
||||
*/
|
||||
//public serverInfo: ConnectionContracts.ServerInfo;
|
||||
|
||||
/**
|
||||
* Timer for tracking extension connection time.
|
||||
*/
|
||||
public extensionTimer: StopWatch;
|
||||
|
||||
/**
|
||||
* Timer for tracking service connection time.
|
||||
*/
|
||||
public serviceTimer: StopWatch;
|
||||
|
||||
/**
|
||||
* Timer for tracking intelliSense activation time.
|
||||
*/
|
||||
public intelliSenseTimer: StopWatch;
|
||||
|
||||
/**
|
||||
* Whether the connection is in the process of connecting.
|
||||
*/
|
||||
public connecting: boolean;
|
||||
|
||||
/**
|
||||
* Whether the connection should be deleted after connection is complete.
|
||||
*/
|
||||
public deleted: boolean;
|
||||
|
||||
/**
|
||||
* Information about the connected server.
|
||||
*/
|
||||
serverInfo: data.ServerInfo;
|
||||
|
||||
/**
|
||||
* Owner uri assigned to the connection
|
||||
*/
|
||||
public ownerUri: string;
|
||||
}
|
||||
1311
src/sql/parts/connection/common/connectionManagementService.ts
Normal file
1311
src/sql/parts/connection/common/connectionManagementService.ts
Normal file
File diff suppressed because it is too large
Load Diff
227
src/sql/parts/connection/common/connectionProfile.ts
Normal file
227
src/sql/parts/connection/common/connectionProfile.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ConnectionProfileGroup } from './connectionProfileGroup';
|
||||
import * as data from 'data';
|
||||
import { ProviderConnectionInfo } from 'sql/parts/connection/common/providerConnectionInfo';
|
||||
import * as interfaces from 'sql/parts/connection/common/interfaces';
|
||||
import { equalsIgnoreCase } from 'vs/base/common/strings';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
|
||||
// Concrete implementation of the IConnectionProfile interface
|
||||
|
||||
/**
|
||||
* A concrete implementation of an IConnectionProfile with support for profile creation and validation
|
||||
*/
|
||||
export class ConnectionProfile extends ProviderConnectionInfo implements interfaces.IConnectionProfile {
|
||||
|
||||
public parent: ConnectionProfileGroup = null;
|
||||
private _id: string;
|
||||
public savePassword: boolean;
|
||||
private _groupName: string;
|
||||
public groupId: string;
|
||||
public saveProfile: boolean;
|
||||
|
||||
public isDisconnecting: boolean = false;
|
||||
public constructor(serverCapabilities?: data.DataProtocolServerCapabilities, model?: interfaces.IConnectionProfile) {
|
||||
super(serverCapabilities, model);
|
||||
if (model) {
|
||||
this.groupId = model.groupId;
|
||||
this.groupFullName = model.groupFullName;
|
||||
this.savePassword = model.savePassword;
|
||||
this.saveProfile = model.saveProfile;
|
||||
this._id = model.id;
|
||||
} else {
|
||||
//Default for a new connection
|
||||
this.savePassword = false;
|
||||
this.saveProfile = true;
|
||||
this._groupName = ConnectionProfile.RootGroupName;
|
||||
this._id = generateUuid();
|
||||
}
|
||||
|
||||
this.options['groupId'] = this.groupId;
|
||||
this.options['databaseDisplayName'] = this.databaseName;
|
||||
}
|
||||
|
||||
public matches(other: interfaces.IConnectionProfile): boolean {
|
||||
return other
|
||||
&& this.providerName === other.providerName
|
||||
&& equalsIgnoreCase(this.serverName, other.serverName)
|
||||
&& equalsIgnoreCase(this.databaseName, other.databaseName)
|
||||
&& equalsIgnoreCase(this.userName, other.userName)
|
||||
&& equalsIgnoreCase(this.options['databaseDisplayName'], other.options['databaseDisplayName'])
|
||||
&& this.authenticationType === other.authenticationType
|
||||
&& this.groupId === other.groupId;
|
||||
}
|
||||
|
||||
public generateNewId() {
|
||||
this._id = generateUuid();
|
||||
}
|
||||
|
||||
public getParent(): ConnectionProfileGroup {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
if (!this._id) {
|
||||
this._id = generateUuid();
|
||||
}
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public set id(value: string) {
|
||||
this._id = value;
|
||||
}
|
||||
|
||||
public get groupFullName(): string {
|
||||
return this._groupName;
|
||||
}
|
||||
|
||||
public set groupFullName(value: string) {
|
||||
this._groupName = value;
|
||||
}
|
||||
|
||||
public get isAddedToRootGroup(): boolean {
|
||||
return (this._groupName === ConnectionProfile.RootGroupName);
|
||||
}
|
||||
|
||||
public clone(): ConnectionProfile {
|
||||
let instance = new ConnectionProfile(this._serverCapabilities, this);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public cloneWithNewId(): ConnectionProfile {
|
||||
let instance = this.clone();
|
||||
instance.generateNewId();
|
||||
return instance;
|
||||
}
|
||||
|
||||
public cloneWithDatabase(databaseName: string): ConnectionProfile {
|
||||
let instance = this.cloneWithNewId();
|
||||
instance.databaseName = databaseName;
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static readonly RootGroupName: string = '/';
|
||||
|
||||
public withoutPassword(): ConnectionProfile {
|
||||
let clone = this.clone();
|
||||
clone.password = '';
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a key derived the connections options (providerName, authenticationType, serverName, databaseName, userName, groupid)
|
||||
* This key uniquely identifies a connection in a group
|
||||
* Example: "providerName:MSSQL|authenticationType:|databaseName:database|serverName:server3|userName:user|group:testid"
|
||||
*/
|
||||
public getOptionsKey(): string {
|
||||
let id = super.getOptionsKey();
|
||||
let databaseDisplayName: string = this.options['databaseDisplayName'];
|
||||
if (databaseDisplayName) {
|
||||
id += ProviderConnectionInfo.idSeparator + 'databaseDisplayName' + ProviderConnectionInfo.nameValueSeparator + databaseDisplayName;
|
||||
}
|
||||
|
||||
return id + ProviderConnectionInfo.idSeparator + 'group' + ProviderConnectionInfo.nameValueSeparator + this.groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique id for the connection that doesn't include the group name
|
||||
*/
|
||||
public getConnectionInfoId(): string {
|
||||
return super.getOptionsKey();
|
||||
}
|
||||
|
||||
public onProviderRegistered(serverCapabilities: data.DataProtocolServerCapabilities): void {
|
||||
if (serverCapabilities.providerName === this.providerName) {
|
||||
this.setServerCapabilities(serverCapabilities);
|
||||
}
|
||||
}
|
||||
|
||||
public toIConnectionProfile(): interfaces.IConnectionProfile {
|
||||
let result: interfaces.IConnectionProfile = {
|
||||
serverName: this.serverName,
|
||||
databaseName: this.databaseName,
|
||||
authenticationType: this.authenticationType,
|
||||
getOptionsKey: undefined,
|
||||
matches: undefined,
|
||||
groupId: this.groupId,
|
||||
groupFullName: this.groupFullName,
|
||||
password: this.password,
|
||||
providerName: this.providerName,
|
||||
savePassword: this.savePassword,
|
||||
userName: this.userName,
|
||||
options: this.options,
|
||||
saveProfile: this.saveProfile,
|
||||
id: this.id
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public toConnectionInfo(): data.ConnectionInfo {
|
||||
return {
|
||||
options: this.options
|
||||
};
|
||||
}
|
||||
|
||||
public static createFromStoredProfile(profile: interfaces.IConnectionProfileStore, serverCapabilities: data.DataProtocolServerCapabilities): ConnectionProfile {
|
||||
let connectionInfo = new ConnectionProfile(serverCapabilities, undefined);
|
||||
connectionInfo.options = profile.options;
|
||||
|
||||
// append group ID and original display name to build unique OE session ID
|
||||
connectionInfo.options = profile.options;
|
||||
connectionInfo.options['groupId'] = connectionInfo.groupId;
|
||||
connectionInfo.options['databaseDisplayName'] = connectionInfo.databaseName;
|
||||
|
||||
connectionInfo.groupId = profile.groupId;
|
||||
connectionInfo.providerName = profile.providerName;
|
||||
connectionInfo.saveProfile = true;
|
||||
connectionInfo.savePassword = profile.savePassword;
|
||||
connectionInfo.id = profile.id || generateUuid();
|
||||
return connectionInfo;
|
||||
}
|
||||
|
||||
public static convertToConnectionProfile(serverCapabilities: data.DataProtocolServerCapabilities, conn: interfaces.IConnectionProfile): ConnectionProfile {
|
||||
if (conn) {
|
||||
let connectionProfile: ConnectionProfile = undefined;
|
||||
let connectionProfileInstance = conn as ConnectionProfile;
|
||||
if (connectionProfileInstance && conn instanceof ConnectionProfile) {
|
||||
connectionProfile = connectionProfileInstance;
|
||||
connectionProfile.setServerCapabilities(serverCapabilities);
|
||||
} else {
|
||||
connectionProfile = new ConnectionProfile(serverCapabilities, conn);
|
||||
}
|
||||
|
||||
return connectionProfile;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public static convertToProfileStore(
|
||||
serverCapabilities: data.DataProtocolServerCapabilities,
|
||||
connectionProfile: interfaces.IConnectionProfile): interfaces.IConnectionProfileStore {
|
||||
if (connectionProfile) {
|
||||
let connectionInfo = ConnectionProfile.convertToConnectionProfile(serverCapabilities, connectionProfile);
|
||||
let profile: interfaces.IConnectionProfileStore = {
|
||||
options: {},
|
||||
groupId: connectionProfile.groupId,
|
||||
providerName: connectionInfo.providerName,
|
||||
savePassword: connectionInfo.savePassword,
|
||||
id: connectionInfo.id
|
||||
};
|
||||
|
||||
profile.options = connectionInfo.options;
|
||||
|
||||
return profile;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
183
src/sql/parts/connection/common/connectionProfileGroup.ts
Normal file
183
src/sql/parts/connection/common/connectionProfileGroup.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ConnectionProfile } from './connectionProfile';
|
||||
|
||||
export interface IConnectionProfileGroup {
|
||||
id: string;
|
||||
parentId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class ConnectionProfileGroup implements IConnectionProfileGroup {
|
||||
|
||||
public children: ConnectionProfileGroup[];
|
||||
public connections: ConnectionProfile[];
|
||||
public parentId: string;
|
||||
private _isRenamed: boolean;
|
||||
public constructor(
|
||||
public name: string,
|
||||
public parent: ConnectionProfileGroup,
|
||||
public id: string,
|
||||
public color: string,
|
||||
public description: string
|
||||
) {
|
||||
this.parentId = parent ? parent.id : undefined;
|
||||
if (this.name === ConnectionProfileGroup.RootGroupName) {
|
||||
this.name = '';
|
||||
}
|
||||
}
|
||||
|
||||
public static GroupNameSeparator: string = '/';
|
||||
public static RootGroupName: string = 'ROOT';
|
||||
|
||||
public toObject(): IConnectionProfileGroup {
|
||||
let subgroups = undefined;
|
||||
if (this.children) {
|
||||
subgroups = [];
|
||||
this.children.forEach((group) => {
|
||||
subgroups.push(group.toObject());
|
||||
});
|
||||
}
|
||||
|
||||
return Object.assign({}, { name: this.name, id: this.id, parentId: this.parentId, children: subgroups, color: this.color, description: this.description });
|
||||
}
|
||||
|
||||
public get groupName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public get fullName(): string {
|
||||
let fullName: string = (this.id === 'root') ? undefined : this.name;
|
||||
if (this.parent) {
|
||||
let parentFullName = this.parent.fullName;
|
||||
if (parentFullName) {
|
||||
fullName = parentFullName + ConnectionProfileGroup.GroupNameSeparator + this.name;
|
||||
}
|
||||
}
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public get isRenamed(): boolean {
|
||||
return this._isRenamed;
|
||||
}
|
||||
|
||||
public set isRenamed(val: boolean) {
|
||||
this._isRenamed = val;
|
||||
}
|
||||
|
||||
public hasChildren(): boolean {
|
||||
if ((this.children && this.children.length > 0) || (this.connections && this.connections.length > 0)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getChildren(): any {
|
||||
let allChildren = [];
|
||||
|
||||
if (this.connections) {
|
||||
this.connections.forEach((conn) => {
|
||||
allChildren.push(conn);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.children) {
|
||||
this.children.forEach((group) => {
|
||||
allChildren.push(group);
|
||||
});
|
||||
}
|
||||
return allChildren;
|
||||
}
|
||||
|
||||
public equals(other: any): boolean {
|
||||
if (!(other instanceof ConnectionProfileGroup)) {
|
||||
return false;
|
||||
}
|
||||
return other.id === this.id;
|
||||
}
|
||||
|
||||
public addConnections(connections: ConnectionProfile[]): void {
|
||||
if (!this.connections) {
|
||||
this.connections = [];
|
||||
}
|
||||
connections.forEach((conn) => {
|
||||
this.connections = this.connections.filter((curConn) => { return curConn.id !== conn.id; });
|
||||
conn.parent = this;
|
||||
this.connections.push(conn);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public addGroups(groups: ConnectionProfileGroup[]): void {
|
||||
if (!this.children) {
|
||||
this.children = [];
|
||||
}
|
||||
groups.forEach((group) => {
|
||||
this.children = this.children.filter((grp) => { return group.id !== grp.id; });
|
||||
group.parent = this;
|
||||
this.children.push(group);
|
||||
});
|
||||
}
|
||||
|
||||
public getParent(): ConnectionProfileGroup {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public static getGroupFullNameParts(groupFullName: string): string[] {
|
||||
groupFullName = groupFullName ? groupFullName : '';
|
||||
let groupNames: string[] = groupFullName.split(ConnectionProfileGroup.GroupNameSeparator);
|
||||
groupNames = groupNames.filter(g => !!g);
|
||||
if (groupNames.length === 0) {
|
||||
groupNames.push('ROOT');
|
||||
} else if (groupNames[0].toUpperCase() !== 'ROOT') {
|
||||
groupNames.unshift('ROOT');
|
||||
}
|
||||
groupNames[0] = 'ROOT';
|
||||
return groupNames;
|
||||
}
|
||||
|
||||
public static isRoot(name: string): boolean {
|
||||
return (!name || name.toUpperCase() === ConnectionProfileGroup.RootGroupName ||
|
||||
name === ConnectionProfileGroup.GroupNameSeparator);
|
||||
}
|
||||
|
||||
public static sameGroupName(name1: string, name2: string): boolean {
|
||||
let sameGroupName: boolean =
|
||||
(!name1 && !name2) ||
|
||||
name1.toUpperCase() === name2.toUpperCase() ||
|
||||
(ConnectionProfileGroup.isRoot(name1) && ConnectionProfileGroup.isRoot(name2));
|
||||
|
||||
return sameGroupName;
|
||||
}
|
||||
|
||||
public static getConnectionsInGroup(group: ConnectionProfileGroup): ConnectionProfile[] {
|
||||
let connections = [];
|
||||
if (group.connections) {
|
||||
group.connections.forEach((con) => connections.push(con));
|
||||
}
|
||||
if (group.children) {
|
||||
group.children.forEach((subgroup) => {
|
||||
connections = connections.concat(this.getConnectionsInGroup(subgroup));
|
||||
});
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
public static getSubgroups(group: ConnectionProfileGroup): ConnectionProfileGroup[] {
|
||||
let subgroups = [];
|
||||
if (group && group.children) {
|
||||
group.children.forEach((grp) => subgroups.push(grp));
|
||||
group.children.forEach((subgroup) => {
|
||||
subgroups = subgroups.concat(this.getSubgroups(subgroup));
|
||||
});
|
||||
}
|
||||
return subgroups;
|
||||
}
|
||||
}
|
||||
160
src/sql/parts/connection/common/connectionStatus.ts
Normal file
160
src/sql/parts/connection/common/connectionStatus.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { $, append, show, hide } from 'vs/base/browser/dom';
|
||||
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IEditorInput } from 'vs/platform/editor/common/editor';
|
||||
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
import { IEditorCloseEvent } from 'vs/workbench/common/editor';
|
||||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
|
||||
import { IConnectionManagementService, IConnectionParams } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { ConnectionStatusManager } from 'sql/parts/connection/common/connectionStatusManager';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
|
||||
enum ConnectionActivityStatus {
|
||||
Connected,
|
||||
Disconnected
|
||||
}
|
||||
|
||||
// Contains connection status for each editor
|
||||
class ConnectionStatusEditor {
|
||||
public connectionActivityStatus: ConnectionActivityStatus;
|
||||
public connectionProfile: IConnectionProfile;
|
||||
|
||||
constructor() {
|
||||
this.connectionActivityStatus = ConnectionActivityStatus.Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
// Connection status bar for editor
|
||||
export class ConnectionStatusbarItem implements IStatusbarItem {
|
||||
|
||||
private _element: HTMLElement;
|
||||
private _connectionElement: HTMLElement;
|
||||
private _connectionStatusEditors: { [connectionUri: string]: ConnectionStatusEditor };
|
||||
private _toDispose: IDisposable[];
|
||||
private _connectionStatusManager: ConnectionStatusManager;
|
||||
|
||||
constructor(
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IEditorGroupService private _editorGroupService: IEditorGroupService,
|
||||
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
||||
) {
|
||||
this._connectionStatusEditors = {};
|
||||
this._connectionStatusManager = new ConnectionStatusManager(this._capabilitiesService);
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): IDisposable {
|
||||
this._element = append(container, $('.connection-statusbar-item'));
|
||||
this._connectionElement = append(this._element, $('div.connection-statusbar-conninfo'));
|
||||
hide(this._connectionElement);
|
||||
|
||||
this._toDispose = [];
|
||||
this._toDispose.push(
|
||||
this._connectionManagementService.onConnect((connectionUri: IConnectionParams) => this._onConnect(connectionUri)),
|
||||
this._connectionManagementService.onConnectionChanged((connectionUri: IConnectionParams) => this._onConnect(connectionUri)),
|
||||
this._connectionManagementService.onDisconnect((connectionUri: IConnectionParams) => this._onDisconnect(connectionUri)),
|
||||
this._editorGroupService.onEditorsChanged(() => this._onEditorsChanged()),
|
||||
this._editorGroupService.getStacksModel().onEditorClosed(event => this._onEditorClosed(event))
|
||||
);
|
||||
|
||||
return combinedDisposable(this._toDispose);
|
||||
}
|
||||
|
||||
private _onEditorClosed(event: IEditorCloseEvent): void {
|
||||
let uri = WorkbenchUtils.getEditorUri(event.editor);
|
||||
if (uri && uri in this._connectionStatusEditors) {
|
||||
this._updateStatus(uri, ConnectionActivityStatus.Disconnected, undefined);
|
||||
delete this._connectionStatusEditors[uri];
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorsChanged(): void {
|
||||
let activeEditor = this._editorService.getActiveEditor();
|
||||
if (activeEditor) {
|
||||
let uri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
|
||||
// Show active editor's query status
|
||||
if (uri && uri in this._connectionStatusEditors) {
|
||||
this._showStatus(uri);
|
||||
} else {
|
||||
hide(this._connectionElement);
|
||||
}
|
||||
} else {
|
||||
hide(this._connectionElement);
|
||||
}
|
||||
}
|
||||
|
||||
private _onConnect(connectionParams: IConnectionParams): void {
|
||||
if (!this._connectionStatusManager.isDefaultTypeUri(connectionParams.connectionUri)) {
|
||||
this._updateStatus(connectionParams.connectionUri, ConnectionActivityStatus.Connected, connectionParams.connectionProfile);
|
||||
}
|
||||
}
|
||||
|
||||
private _onDisconnect(connectionUri: IConnectionParams): void {
|
||||
if (!this._connectionStatusManager.isDefaultTypeUri(connectionUri.connectionUri)) {
|
||||
this._updateStatus(connectionUri.connectionUri, ConnectionActivityStatus.Disconnected, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// Update connection status for the editor
|
||||
private _updateStatus(uri: string, newStatus: ConnectionActivityStatus, connectionProfile: IConnectionProfile) {
|
||||
if (uri) {
|
||||
if (!(uri in this._connectionStatusEditors)) {
|
||||
this._connectionStatusEditors[uri] = new ConnectionStatusEditor();
|
||||
}
|
||||
this._connectionStatusEditors[uri].connectionActivityStatus = newStatus;
|
||||
this._connectionStatusEditors[uri].connectionProfile = connectionProfile;
|
||||
this._showStatus(uri);
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide query status for active editor
|
||||
private _showStatus(uri: string): void {
|
||||
let activeEditor = this._editorService.getActiveEditor();
|
||||
if (activeEditor) {
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (uri === currentUri) {
|
||||
switch (this._connectionStatusEditors[uri].connectionActivityStatus) {
|
||||
case ConnectionActivityStatus.Connected:
|
||||
this._setConnectionText(this._connectionStatusEditors[uri].connectionProfile);
|
||||
show(this._connectionElement);
|
||||
break;
|
||||
case ConnectionActivityStatus.Disconnected:
|
||||
hide(this._connectionElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set connection info to connection status bar
|
||||
private _setConnectionText(connectionProfile: IConnectionProfile): void {
|
||||
let text: string = connectionProfile.serverName;
|
||||
if (text) {
|
||||
if (connectionProfile.databaseName && connectionProfile.databaseName !== '') {
|
||||
text = text + ' : ' + connectionProfile.databaseName;
|
||||
} else {
|
||||
text = text + ' : ' + '<default>';
|
||||
}
|
||||
}
|
||||
|
||||
let tooltip: string =
|
||||
'Server name: ' + connectionProfile.serverName + '\r\n' +
|
||||
'Database name: ' + (connectionProfile.databaseName ? connectionProfile.databaseName : '<default>') + '\r\n';
|
||||
|
||||
if (connectionProfile.userName && connectionProfile.userName !== '') {
|
||||
tooltip = tooltip + 'Login name: ' + connectionProfile.userName + '\r\n';
|
||||
}
|
||||
|
||||
this._connectionElement.textContent = text;
|
||||
this._connectionElement.title = tooltip;
|
||||
}
|
||||
}
|
||||
212
src/sql/parts/connection/common/connectionStatusManager.ts
Normal file
212
src/sql/parts/connection/common/connectionStatusManager.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ConnectionManagementInfo } from './connectionManagementInfo';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
|
||||
import { IConnectionProfile } from './interfaces';
|
||||
import * as Utils from './utils';
|
||||
import * as data from 'data';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
|
||||
export class ConnectionStatusManager {
|
||||
|
||||
private _connections: { [id: string]: ConnectionManagementInfo };
|
||||
private _providerCapabilitiesMap: { [providerName: string]: data.DataProtocolServerCapabilities };
|
||||
|
||||
constructor( @ICapabilitiesService private _capabilitiesService: ICapabilitiesService) {
|
||||
this._connections = {};
|
||||
this._providerCapabilitiesMap = {};
|
||||
}
|
||||
|
||||
public getCapabilities(providerName: string): data.DataProtocolServerCapabilities {
|
||||
let result: data.DataProtocolServerCapabilities;
|
||||
|
||||
if (providerName in this._providerCapabilitiesMap) {
|
||||
result = this._providerCapabilitiesMap[providerName];
|
||||
} else {
|
||||
let capabilities = this._capabilitiesService.getCapabilities();
|
||||
if (capabilities) {
|
||||
let providerCapabilities = capabilities.find(c => c.providerName === providerName);
|
||||
if (providerCapabilities) {
|
||||
this._providerCapabilitiesMap[providerName] = providerCapabilities;
|
||||
result = providerCapabilities;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public findConnection(id: string): ConnectionManagementInfo {
|
||||
if (id in this._connections) {
|
||||
return this._connections[id];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public findConnectionProfile(connectionProfile: IConnectionProfile): ConnectionManagementInfo {
|
||||
let id = Utils.generateUri(connectionProfile);
|
||||
return this.findConnection(id);
|
||||
}
|
||||
|
||||
public hasConnection(id: string): Boolean {
|
||||
return !!this.findConnection(id);
|
||||
}
|
||||
|
||||
public deleteConnection(id: string): void {
|
||||
let info = this.findConnection(id);
|
||||
if (info) {
|
||||
for (let key in this._connections) {
|
||||
if (this._connections[key].connectionId === info.connectionId) {
|
||||
if (this._connections[key].connecting) {
|
||||
this._connections[key].deleted = true;
|
||||
} else {
|
||||
delete this._connections[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getConnectionProfile(id: string): ConnectionProfile {
|
||||
let connectionInfoForId = this.findConnection(id);
|
||||
return connectionInfoForId ? connectionInfoForId.connectionProfile : undefined;
|
||||
}
|
||||
|
||||
public addConnection(connection: IConnectionProfile, id: string): ConnectionManagementInfo {
|
||||
// Always create a copy and save that in the list
|
||||
let connectionProfile = new ConnectionProfile(this.getCapabilities(connection.providerName), connection);
|
||||
const self = this;
|
||||
let connectionInfo: ConnectionManagementInfo = new ConnectionManagementInfo();
|
||||
connectionInfo.providerId = connection.providerName;
|
||||
connectionInfo.extensionTimer = StopWatch.create();
|
||||
connectionInfo.intelliSenseTimer = StopWatch.create();
|
||||
connectionInfo.connectionProfile = connectionProfile;
|
||||
connectionInfo.connecting = true;
|
||||
self._connections[id] = connectionInfo;
|
||||
connectionInfo.serviceTimer = StopWatch.create();
|
||||
connectionInfo.ownerUri = id;
|
||||
|
||||
return connectionInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param uri Remove connection from list of active connections
|
||||
*/
|
||||
public removeConnection(uri: string) {
|
||||
delete this._connections[uri];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after a connection is saved to settings. It's only for default url connections
|
||||
* which their id is generated from connection options. The group id is used in the generated id.
|
||||
* when the connection is stored, the group id get assigned to the profile and it can change the id
|
||||
* So for those kind of connections, we need to add the new id and the connection
|
||||
*/
|
||||
public updateConnectionProfile(connection: IConnectionProfile, id: string): string {
|
||||
let newId: string = id;
|
||||
let connectionInfo: ConnectionManagementInfo = this._connections[id];
|
||||
if (connectionInfo && connection) {
|
||||
if (this.isDefaultTypeUri(id)) {
|
||||
connectionInfo.connectionProfile.groupId = connection.groupId;
|
||||
newId = Utils.generateUri(connection);
|
||||
if (newId !== id) {
|
||||
this.deleteConnection(id);
|
||||
this._connections[newId] = connectionInfo;
|
||||
}
|
||||
}
|
||||
connectionInfo.connectionProfile.id = connection.id;
|
||||
}
|
||||
return newId;
|
||||
}
|
||||
|
||||
public onConnectionComplete(summary: data.ConnectionInfoSummary): ConnectionManagementInfo {
|
||||
let connection = this._connections[summary.ownerUri];
|
||||
connection.serviceTimer.stop();
|
||||
connection.connecting = false;
|
||||
connection.connectionId = summary.connectionId;
|
||||
connection.serverInfo = summary.serverInfo;
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates database name after connection is complete
|
||||
* @param summary connection summary
|
||||
*/
|
||||
public updateDatabaseName(summary: data.ConnectionInfoSummary): void {
|
||||
let connection = this._connections[summary.ownerUri];
|
||||
|
||||
//Check if the existing connection database name is different the one in the summary
|
||||
if (connection.connectionProfile.databaseName !== summary.connectionSummary.databaseName) {
|
||||
//Add the ownerUri with database name to the map if not already exists
|
||||
connection.connectionProfile.databaseName = summary.connectionSummary.databaseName;
|
||||
let prefix = Utils.getUriPrefix(summary.ownerUri);
|
||||
let ownerUriWithDbName = Utils.generateUriWithPrefix(connection.connectionProfile, prefix);
|
||||
if (!(ownerUriWithDbName in this._connections)) {
|
||||
this._connections[ownerUriWithDbName] = connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find an existing connection that's mapped with given the 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
|
||||
* 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
|
||||
*/
|
||||
public getOriginalOwnerUri(ownerUri: string): string {
|
||||
let ownerUriToReturn: string = ownerUri;
|
||||
|
||||
let connectionStatusInfo = this.findConnection(ownerUriToReturn);
|
||||
if (connectionStatusInfo && connectionStatusInfo.ownerUri) {
|
||||
//The ownerUri in the connection status is the one service knows about so use that
|
||||
//To call the service for any operation
|
||||
ownerUriToReturn = connectionStatusInfo.ownerUri;
|
||||
}
|
||||
return ownerUriToReturn;
|
||||
}
|
||||
|
||||
public onConnectionChanged(changedConnInfo: data.ChangedConnectionInfo): IConnectionProfile {
|
||||
let connection = this._connections[changedConnInfo.connectionUri];
|
||||
if (connection && connection.connectionProfile) {
|
||||
connection.connectionProfile.serverName = changedConnInfo.connection.serverName;
|
||||
connection.connectionProfile.databaseName = changedConnInfo.connection.databaseName;
|
||||
connection.connectionProfile.userName = changedConnInfo.connection.userName;
|
||||
return connection.connectionProfile;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public isConnected(id: string): boolean {
|
||||
return (id in this._connections && this._connections[id].connectionId && !!this._connections[id].connectionId);
|
||||
}
|
||||
|
||||
public isConnecting(id: string): boolean {
|
||||
return (id in this._connections && this._connections[id].connecting);
|
||||
}
|
||||
|
||||
public isDefaultTypeUri(uri: string): boolean {
|
||||
return uri && uri.startsWith(Utils.uriPrefixes.default);
|
||||
}
|
||||
|
||||
public getProviderIdFromUri(ownerUri: string): string {
|
||||
let providerId: string = '';
|
||||
let connection = this.findConnection(ownerUri);
|
||||
if (connection) {
|
||||
providerId = connection.connectionProfile.providerName;
|
||||
}
|
||||
if (!providerId && this.isDefaultTypeUri(ownerUri)) {
|
||||
let optionsKey = ownerUri.replace(Utils.uriPrefixes.default, '');
|
||||
providerId = ConnectionProfile.getProviderFromOptionsKey(optionsKey);
|
||||
}
|
||||
return providerId;
|
||||
}
|
||||
}
|
||||
616
src/sql/parts/connection/common/connectionStore.ts
Normal file
616
src/sql/parts/connection/common/connectionStore.ts
Normal file
@@ -0,0 +1,616 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as Constants from './constants';
|
||||
import * as ConnInfo from './connectionInfo';
|
||||
import { ConnectionProfile } from '../common/connectionProfile';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { ICredentialsService } from 'sql/services/credentials/credentialsService';
|
||||
import { IConnectionConfig } from './iconnectionConfig';
|
||||
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 { 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';
|
||||
|
||||
const MAX_CONNECTIONS_DEFAULT = 25;
|
||||
|
||||
/**
|
||||
* Manages the connections list including saved profiles and the most recently used connections
|
||||
*
|
||||
* @export
|
||||
* @class ConnectionStore
|
||||
*/
|
||||
export class ConnectionStore {
|
||||
private _memento: any;
|
||||
private _groupIdToFullNameMap: { [groupId: string]: string };
|
||||
private _groupFullNameToIdMap: { [groupId: string]: string };
|
||||
|
||||
constructor(
|
||||
private _storageService: IStorageService,
|
||||
private _context: Memento,
|
||||
private _configurationEditService: IConfigurationEditingService,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public static get CRED_PREFIX(): string { return 'Microsoft.SqlTools'; }
|
||||
public static get CRED_SEPARATOR(): string { return '|'; }
|
||||
public static get CRED_ID_PREFIX(): string { return 'id:'; }
|
||||
public static get CRED_ITEMTYPE_PREFIX(): string { return 'itemtype:'; }
|
||||
public static get CRED_PROFILE_USER(): string { return 'Profile'; }
|
||||
|
||||
public formatCredentialIdForCred(connectionProfile: IConnectionProfile): string {
|
||||
if (!connectionProfile) {
|
||||
throw new Error('Missing Connection which is required');
|
||||
}
|
||||
let itemTypeString: string = ConnectionStore.CRED_PROFILE_USER;
|
||||
return this.formatCredentialId(connectionProfile, itemTypeString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted credential usable for uniquely identifying a SQL Connection.
|
||||
* This string can be decoded but is not optimized for this.
|
||||
* @static
|
||||
* @param {IConnectionProfile} connectionProfile connection profile - require
|
||||
* @param {string} itemType type of the item (MRU or Profile) - optional
|
||||
* @returns {string} formatted string with server, DB and username
|
||||
*/
|
||||
public formatCredentialId(connectionProfile: IConnectionProfile, itemType?: string): string {
|
||||
let connectionProfileInstance: ConnectionProfile = ConnectionProfile.convertToConnectionProfile(
|
||||
this._connectionConfig.getCapabilities(connectionProfile.providerName), connectionProfile);
|
||||
if (!connectionProfileInstance.getConnectionInfoId()) {
|
||||
throw new Error('Missing Id, which is required');
|
||||
}
|
||||
let cred: string[] = [ConnectionStore.CRED_PREFIX];
|
||||
if (!itemType) {
|
||||
itemType = ConnectionStore.CRED_PROFILE_USER;
|
||||
}
|
||||
|
||||
ConnectionStore.pushIfNonEmpty(itemType, ConnectionStore.CRED_ITEMTYPE_PREFIX, cred);
|
||||
ConnectionStore.pushIfNonEmpty(connectionProfileInstance.getConnectionInfoId(), ConnectionStore.CRED_ID_PREFIX, cred);
|
||||
return cred.join(ConnectionStore.CRED_SEPARATOR);
|
||||
}
|
||||
|
||||
private static pushIfNonEmpty(value: string, prefix: string, arr: string[]): void {
|
||||
if (value) {
|
||||
arr.push(prefix.concat(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the password is required
|
||||
* @param connection profile
|
||||
*/
|
||||
public isPasswordRequired(connection: IConnectionProfile): boolean {
|
||||
if (connection) {
|
||||
let connectionProfile = ConnectionProfile.convertToConnectionProfile(this._connectionConfig.getCapabilities(connection.providerName), connection);
|
||||
return connectionProfile.isPasswordRequired();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
if (credentialsItem.savePassword && this.isPasswordRequired(credentialsItem)
|
||||
&& !credentialsItem.password) {
|
||||
|
||||
let credentialId = this.formatCredentialIdForCred(credentialsItem);
|
||||
self._credentialService.readCredential(credentialId)
|
||||
.then(savedCred => {
|
||||
if (savedCred) {
|
||||
credentialsItem.password = savedCred.password;
|
||||
}
|
||||
resolve({ profile: credentialsItem, savedCred: !!savedCred });
|
||||
},
|
||||
reason => {
|
||||
reject(reason);
|
||||
});
|
||||
} else {
|
||||
// No need to look up the password
|
||||
resolve({ profile: credentialsItem, savedCred: credentialsItem.savePassword });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a connection profile to the user settings.
|
||||
* Password values are stored to a separate credential store if the "savePassword" option is true
|
||||
*
|
||||
* @param {IConnectionProfile} profile the profile to save
|
||||
* @param {forceWritePlaintextPassword} whether the plaintext password should be written to the settings file
|
||||
* @returns {Promise<IConnectionProfile>} a Promise that returns the original profile, for help in chaining calls
|
||||
*/
|
||||
public saveProfile(profile: IConnectionProfile, forceWritePlaintextPassword?: boolean): Promise<IConnectionProfile> {
|
||||
const self = this;
|
||||
return new Promise<IConnectionProfile>((resolve, reject) => {
|
||||
// Add the profile to the saved list, taking care to clear out the password field if necessary
|
||||
let savedProfile: IConnectionProfile;
|
||||
if (forceWritePlaintextPassword) {
|
||||
savedProfile = profile;
|
||||
} else {
|
||||
|
||||
savedProfile = this.getProfileWithoutPassword(profile);
|
||||
}
|
||||
self.saveProfileToConfig(savedProfile)
|
||||
.then(savedConnectionProfile => {
|
||||
profile.groupId = savedConnectionProfile.groupId;
|
||||
profile.id = savedConnectionProfile.id;
|
||||
// Only save if we successfully added the profile
|
||||
return self.saveProfilePasswordIfNeeded(profile);
|
||||
// And resolve / reject at the end of the process
|
||||
}, err => {
|
||||
reject(err);
|
||||
}).then(resolved => {
|
||||
// Add necessary default properties before returning
|
||||
// this is needed to support immediate connections
|
||||
ConnInfo.fixupConnectionCredentials(profile);
|
||||
this.saveCachedServerCapabilities();
|
||||
resolve(profile);
|
||||
}, err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a connection profile group to the user settings.
|
||||
*
|
||||
* @param {IConnectionProfileGroup} profile the profile group to save
|
||||
* @returns {Promise<string>} a Promise that returns the id of connection group
|
||||
*/
|
||||
public saveProfileGroup(profile: IConnectionProfileGroup): Promise<string> {
|
||||
const self = this;
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
self._connectionConfig.addGroup(profile).then(groupId => {
|
||||
resolve(groupId);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private saveProfileToConfig(profile: IConnectionProfile): Promise<IConnectionProfile> {
|
||||
const self = this;
|
||||
return new Promise<IConnectionProfile>((resolve, reject) => {
|
||||
if (profile.saveProfile) {
|
||||
self._connectionConfig.addConnection(profile).then(savedProfile => {
|
||||
resolve(savedProfile);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
resolve(profile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getCachedServerCapabilities(): data.DataProtocolServerCapabilities[] {
|
||||
if (this._memento) {
|
||||
let metadata: data.DataProtocolServerCapabilities[] = this._memento[Constants.capabilitiesOptions];
|
||||
return metadata;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private saveCachedServerCapabilities(): void {
|
||||
if (this._memento) {
|
||||
let capabilities = this._capabilitiesService.getCapabilities();
|
||||
this._memento[Constants.capabilitiesOptions] = capabilities;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of recently used connections. These will not include the password - a separate call to
|
||||
* {addSavedPassword} is needed to fill that before connecting
|
||||
*
|
||||
* @returns {data.ConnectionInfo} the array of connections, empty if none are found
|
||||
*/
|
||||
public getRecentlyUsedConnections(): ConnectionProfile[] {
|
||||
let configValues: IConnectionProfile[] = this._memento[Constants.recentConnections];
|
||||
if (!configValues) {
|
||||
configValues = [];
|
||||
}
|
||||
|
||||
configValues = configValues.filter(c => !!(c));
|
||||
return this.convertConfigValuesToConnectionProfiles(configValues);
|
||||
}
|
||||
|
||||
private convertConfigValuesToConnectionProfiles(configValues: IConnectionProfile[]): ConnectionProfile[] {
|
||||
return configValues.map(c => {
|
||||
if (c) {
|
||||
let capabilities = this._connectionConfig.getCapabilities(c.providerName);
|
||||
let connectionProfile = new ConnectionProfile(capabilities, c);
|
||||
this._capabilitiesService.onProviderRegisteredEvent((serverCapabilities) => {
|
||||
connectionProfile.onProviderRegistered(serverCapabilities);
|
||||
});
|
||||
if (connectionProfile.saveProfile) {
|
||||
if (!connectionProfile.groupFullName && connectionProfile.groupId) {
|
||||
connectionProfile.groupFullName = this.getGroupFullName(connectionProfile.groupId);
|
||||
}
|
||||
if (!connectionProfile.groupId && connectionProfile.groupFullName) {
|
||||
connectionProfile.groupId = this.getGroupId(connectionProfile.groupFullName);
|
||||
} else if (!connectionProfile.groupId && !connectionProfile.groupFullName) {
|
||||
connectionProfile.groupId = this.getGroupId('');
|
||||
}
|
||||
}
|
||||
return connectionProfile;
|
||||
} else {
|
||||
return undefined;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of active connections. These will not include the password - a separate call to
|
||||
* {addSavedPassword} is needed to fill that before connecting
|
||||
*
|
||||
* @returns {data.ConnectionInfo} the array of connections, empty if none are found
|
||||
*/
|
||||
public getActiveConnections(): ConnectionProfile[] {
|
||||
let configValues: IConnectionProfile[] = this._memento[Constants.activeConnections];
|
||||
if (!configValues) {
|
||||
configValues = [];
|
||||
}
|
||||
|
||||
return this.convertConfigValuesToConnectionProfiles(configValues);
|
||||
}
|
||||
|
||||
public getProfileWithoutPassword(conn: IConnectionProfile): ConnectionProfile {
|
||||
if (conn) {
|
||||
let savedConn: ConnectionProfile = ConnectionProfile.convertToConnectionProfile(this._connectionConfig.getCapabilities(conn.providerName), conn);
|
||||
savedConn = savedConn.withoutPassword();
|
||||
|
||||
return savedConn;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a connection to the active connections list.
|
||||
* Connection is only added if there are no other connections with the same connection ID in the list.
|
||||
* Password values are stored to a separate credential store if the "savePassword" option is true
|
||||
*
|
||||
* @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)) {
|
||||
return Promise.resolve(undefined);
|
||||
} else {
|
||||
return this.addConnectionToMemento(conn, Constants.activeConnections, undefined, conn.savePassword).then(() => {
|
||||
let maxConnections = this.getMaxRecentConnectionsCount();
|
||||
return this.addConnectionToMemento(conn, Constants.recentConnections, maxConnections);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public addConnectionToMemento(conn: IConnectionProfile, mementoKey: string, maxConnections?: number, savePassword?: boolean): Promise<void> {
|
||||
const self = this;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// Get all profiles
|
||||
let configValues = self.getConnectionsFromMemento(mementoKey);
|
||||
let configToSave: IConnectionProfile[] = this.addToConnectionList(conn, configValues, mementoKey === Constants.recentConnections);
|
||||
|
||||
if (maxConnections) {
|
||||
// Remove last element if needed
|
||||
if (configToSave.length > maxConnections) {
|
||||
configToSave = configToSave.slice(0, maxConnections);
|
||||
}
|
||||
}
|
||||
self._memento[mementoKey] = configToSave;
|
||||
if (savePassword) {
|
||||
self.doSavePassword(conn).then(result => {
|
||||
resolve(undefined);
|
||||
});
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
// Get all profiles
|
||||
let configValues = self.getConnectionsFromMemento(mementoKey);
|
||||
let configToSave = this.removeFromConnectionList(conn, configValues);
|
||||
|
||||
self._memento[mementoKey] = configToSave;
|
||||
resolve(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
public getConnectionsFromMemento(mementoKey: string): ConnectionProfile[] {
|
||||
let configValues: IConnectionProfile[] = this._memento[mementoKey];
|
||||
if (!configValues) {
|
||||
configValues = [];
|
||||
}
|
||||
|
||||
return this.convertConfigValuesToConnectionProfiles(configValues);
|
||||
}
|
||||
|
||||
private addToConnectionList(conn: IConnectionProfile, list: ConnectionProfile[], isRecentConnections: boolean): 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.unshift(savedProfile);
|
||||
|
||||
let newList = list.map(c => {
|
||||
let connectionProfile = c ? c.toIConnectionProfile() : undefined;;
|
||||
return connectionProfile;
|
||||
});
|
||||
return newList.filter(n => n !== undefined);
|
||||
}
|
||||
|
||||
private removeFromConnectionList(conn: IConnectionProfile, list: ConnectionProfile[]): IConnectionProfile[] {
|
||||
let savedProfile: ConnectionProfile = this.getProfileWithoutPassword(conn);
|
||||
|
||||
// Remove the connection from the list if it already exists
|
||||
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;
|
||||
});
|
||||
|
||||
let newList = list.map(c => {
|
||||
let connectionProfile = c ? c.toIConnectionProfile() : undefined;;
|
||||
return connectionProfile;
|
||||
});
|
||||
return newList.filter(n => n !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all recently used connections from the MRU list.
|
||||
*/
|
||||
public clearRecentlyUsed(): void {
|
||||
this._memento[Constants.recentConnections] = [];
|
||||
}
|
||||
|
||||
public clearFromMemento(name: string): void {
|
||||
this._memento[name] = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear all active connections from the MRU list.
|
||||
*/
|
||||
public clearActiveConnections(): void {
|
||||
this._memento[Constants.activeConnections] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a connection profile from the active connections list.
|
||||
*/
|
||||
public removeActiveConnection(conn: IConnectionProfile): Promise<void> {
|
||||
return this.removeConnectionToMemento(conn, Constants.activeConnections);
|
||||
}
|
||||
|
||||
private saveProfilePasswordIfNeeded(profile: IConnectionProfile): Promise<boolean> {
|
||||
if (!profile.savePassword) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return this.doSavePassword(profile);
|
||||
}
|
||||
|
||||
private doSavePassword(conn: IConnectionProfile): Promise<boolean> {
|
||||
let self = this;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
if (conn.password) {
|
||||
let credentialId = this.formatCredentialId(conn);
|
||||
self._credentialService.saveCredential(credentialId, conn.password)
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
}, reason => {
|
||||
// Bubble up error if there was a problem executing the set command
|
||||
reject(reason);
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getConnectionProfileGroups(withoutConnections?: boolean): ConnectionProfileGroup[] {
|
||||
let profilesInConfiguration: ConnectionProfile[];
|
||||
if (!withoutConnections) {
|
||||
profilesInConfiguration = this._connectionConfig.getConnections(true);
|
||||
}
|
||||
let groups = this._connectionConfig.getAllGroups();
|
||||
|
||||
let connectionProfileGroups = this.convertToConnectionGroup(groups, profilesInConfiguration, undefined);
|
||||
return connectionProfileGroups;
|
||||
}
|
||||
|
||||
private convertToConnectionGroup(groups: IConnectionProfileGroup[], connections: ConnectionProfile[], parent: ConnectionProfileGroup = undefined): ConnectionProfileGroup[] {
|
||||
let result: ConnectionProfileGroup[] = [];
|
||||
let children = groups.filter(g => g.parentId === (parent ? parent.id : undefined));
|
||||
if (children) {
|
||||
children.map(group => {
|
||||
let connectionGroup = new ConnectionProfileGroup(group.name, parent, group.id, group.color, group.description);
|
||||
this.addGroupFullNameToMap(group.id, connectionGroup.fullName);
|
||||
if (connections) {
|
||||
let connectionsForGroup = connections.filter(conn => conn.groupId === connectionGroup.id);
|
||||
var conns = [];
|
||||
connectionsForGroup.forEach((conn) => {
|
||||
conn.groupFullName = connectionGroup.fullName;
|
||||
conns.push(conn);
|
||||
});
|
||||
connectionGroup.addConnections(conns);
|
||||
}
|
||||
|
||||
let childrenGroups = this.convertToConnectionGroup(groups, connections, connectionGroup);
|
||||
connectionGroup.addGroups(childrenGroups);
|
||||
result.push(connectionGroup);
|
||||
});
|
||||
if (parent) {
|
||||
parent.addGroups(result);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
let maxConnections: number = config[Constants.configMaxRecentConnections];
|
||||
if (typeof (maxConnections) !== 'number' || maxConnections <= 0) {
|
||||
maxConnections = MAX_CONNECTIONS_DEFAULT;
|
||||
}
|
||||
return maxConnections;
|
||||
}
|
||||
|
||||
public editGroup(group: ConnectionProfileGroup): Promise<any> {
|
||||
const self = this;
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
self._connectionConfig.editGroup(group).then(() => {
|
||||
resolve(null);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public deleteConnectionFromConfiguration(connection: ConnectionProfile): Promise<void> {
|
||||
return this._connectionConfig.deleteConnection(connection);
|
||||
}
|
||||
|
||||
public deleteGroupFromConfiguration(group: ConnectionProfileGroup): Promise<void> {
|
||||
return this._connectionConfig.deleteGroup(group);
|
||||
}
|
||||
|
||||
public changeGroupIdForConnectionGroup(source: ConnectionProfileGroup, target: ConnectionProfileGroup): Promise<void> {
|
||||
return this._connectionConfig.changeGroupIdForConnectionGroup(source, target);
|
||||
}
|
||||
|
||||
public canChangeConnectionConfig(profile: ConnectionProfile, newGroupID: string): boolean {
|
||||
return this._connectionConfig.canChangeConnectionConfig(profile, newGroupID);
|
||||
}
|
||||
|
||||
public changeGroupIdForConnection(source: ConnectionProfile, targetGroupId: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this._connectionConfig.changeGroupIdForConnection(source, targetGroupId).then(() => {
|
||||
resolve();
|
||||
}, (error => {
|
||||
reject(error);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private addGroupFullNameToMap(groupId: string, groupFullName: string): void {
|
||||
if (groupId) {
|
||||
this._groupIdToFullNameMap[groupId] = groupFullName;
|
||||
}
|
||||
if (groupFullName !== undefined) {
|
||||
this._groupFullNameToIdMap[groupFullName.toUpperCase()] = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
private getGroupFullName(groupId: string): string {
|
||||
if (groupId in this._groupIdToFullNameMap) {
|
||||
return this._groupIdToFullNameMap[groupId];
|
||||
} else {
|
||||
// Load the cache
|
||||
this.getConnectionProfileGroups(true);
|
||||
}
|
||||
return this._groupIdToFullNameMap[groupId];
|
||||
}
|
||||
|
||||
private getGroupId(groupFullName: string): string {
|
||||
if (groupFullName === ConnectionProfileGroup.GroupNameSeparator) {
|
||||
groupFullName = '';
|
||||
}
|
||||
let key = groupFullName.toUpperCase();
|
||||
let result: string = '';
|
||||
if (key in this._groupFullNameToIdMap) {
|
||||
result = this._groupFullNameToIdMap[key];
|
||||
} else {
|
||||
// Load the cache
|
||||
this.getConnectionProfileGroups(true);
|
||||
result = this._groupFullNameToIdMap[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
27
src/sql/parts/connection/common/constants.ts
Normal file
27
src/sql/parts/connection/common/constants.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// constants
|
||||
export const sqlConfigSectionName = 'sql';
|
||||
export const outputChannelName = 'MSSQL';
|
||||
|
||||
export const connectionsArrayName = 'datasource.connections';
|
||||
export const connectionGroupsArrayName = 'datasource.connectionGroups';
|
||||
|
||||
/**Unsaved connections Id */
|
||||
export const unsavedGroupId = 'unsaved';
|
||||
|
||||
/* Memento constants */
|
||||
export const activeConnections = 'ACTIVE_CONNECTIONS';
|
||||
export const recentConnections = 'RECENT_CONNECTIONS';
|
||||
export const capabilitiesOptions = 'OPTIONS_METADATA';
|
||||
|
||||
export const configMaxRecentConnections = 'maxRecentConnections';
|
||||
|
||||
export const mssqlProviderName = 'MSSQL';
|
||||
|
||||
export const applicationName = 'sqlops';
|
||||
|
||||
export const defaultEngine = 'defaultEngine';
|
||||
32
src/sql/parts/connection/common/iconnectionConfig.ts
Normal file
32
src/sql/parts/connection/common/iconnectionConfig.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IConnectionProfile } from './interfaces';
|
||||
import { IConnectionProfileGroup, ConnectionProfileGroup } from './connectionProfileGroup';
|
||||
import { ConnectionProfile } from './connectionProfile';
|
||||
import data = require('data');
|
||||
|
||||
/**
|
||||
* Interface for a configuration file that stores connection profiles.
|
||||
*
|
||||
* @export
|
||||
* @interface IConnectionConfig
|
||||
*/
|
||||
export interface IConnectionConfig {
|
||||
addConnection(profile: IConnectionProfile): Promise<IConnectionProfile>;
|
||||
addGroup(profileGroup: IConnectionProfileGroup): Promise<string>;
|
||||
getConnections(getWorkspaceConnections: boolean): ConnectionProfile[];
|
||||
getAllGroups(): IConnectionProfileGroup[];
|
||||
changeGroupIdForConnectionGroup(source: ConnectionProfileGroup, target: ConnectionProfileGroup): Promise<void>;
|
||||
changeGroupIdForConnection(source: ConnectionProfile, targetGroupId: string): Promise<void>;
|
||||
setCachedMetadata(cachedMetaData: data.DataProtocolServerCapabilities[]): void;
|
||||
getCapabilities(providerName: string): data.DataProtocolServerCapabilities;
|
||||
editGroup(group: ConnectionProfileGroup): Promise<void>;
|
||||
deleteConnection(profile: ConnectionProfile): Promise<void>;
|
||||
deleteGroup(group: ConnectionProfileGroup): Promise<void>;
|
||||
canChangeConnectionConfig(profile: ConnectionProfile, newGroupID: string): boolean;
|
||||
}
|
||||
34
src/sql/parts/connection/common/interfaces.ts
Normal file
34
src/sql/parts/connection/common/interfaces.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as data from 'data';
|
||||
|
||||
// A Connection Profile contains all the properties of connection credentials, with additional
|
||||
// optional name and details on whether password should be saved
|
||||
export interface IConnectionProfile extends data.ConnectionInfo {
|
||||
serverName: string;
|
||||
databaseName: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
authenticationType: string;
|
||||
savePassword: boolean;
|
||||
groupFullName: string;
|
||||
groupId: string;
|
||||
getOptionsKey(): string;
|
||||
matches(profile: IConnectionProfile): boolean;
|
||||
providerName: string;
|
||||
saveProfile: boolean;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface IConnectionProfileStore {
|
||||
options: {};
|
||||
groupId: string;
|
||||
providerName: string;
|
||||
savePassword: boolean;
|
||||
id: string;
|
||||
};
|
||||
|
||||
10
src/sql/parts/connection/common/localizedConstants.ts
Normal file
10
src/sql/parts/connection/common/localizedConstants.ts
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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export const onDidConnectMessage = localize('onDidConnectMessage', 'Connected to');
|
||||
export const onDidDisconnectMessage = localize('onDidDisconnectMessage', 'Disconnected');
|
||||
export const unsavedGroupLabel = localize('unsavedGroupLabel', 'Unsaved Connections');
|
||||
241
src/sql/parts/connection/common/providerConnectionInfo.ts
Normal file
241
src/sql/parts/connection/common/providerConnectionInfo.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 data = require('data');
|
||||
import * as interfaces from 'sql/parts/connection/common/interfaces';
|
||||
import { ConnectionOptionSpecialType, ServiceOptionType } from 'sql/parts/connection/common/connectionManagement';
|
||||
import * as Constants from 'sql/parts/connection/common/constants';
|
||||
|
||||
export class ProviderConnectionInfo implements data.ConnectionInfo {
|
||||
|
||||
options: { [name: string]: any };
|
||||
|
||||
public providerName: string;
|
||||
protected _serverCapabilities: data.DataProtocolServerCapabilities;
|
||||
private static readonly SqlAuthentication = 'SqlLogin';
|
||||
public static readonly ProviderPropertyName = 'providerName';
|
||||
|
||||
public constructor(serverCapabilities?: data.DataProtocolServerCapabilities, model?: interfaces.IConnectionProfile) {
|
||||
this.options = {};
|
||||
if (serverCapabilities) {
|
||||
this._serverCapabilities = serverCapabilities;
|
||||
this.providerName = serverCapabilities.providerName;
|
||||
}
|
||||
if (model) {
|
||||
if (model.options && this._serverCapabilities) {
|
||||
this._serverCapabilities.connectionProvider.options.forEach(option => {
|
||||
let value = model.options[option.name];
|
||||
this.options[option.name] = value;
|
||||
});
|
||||
}
|
||||
this.serverName = model.serverName;
|
||||
this.authenticationType = model.authenticationType;
|
||||
this.databaseName = model.databaseName;
|
||||
this.password = model.password;
|
||||
this.userName = model.userName;
|
||||
}
|
||||
}
|
||||
|
||||
public clone(): ProviderConnectionInfo {
|
||||
let instance = new ProviderConnectionInfo(this._serverCapabilities);
|
||||
instance.options = Object.assign({}, this.options);
|
||||
instance.providerName = this.providerName;
|
||||
return instance;
|
||||
}
|
||||
|
||||
public get serverCapabilities(): data.DataProtocolServerCapabilities {
|
||||
return this._serverCapabilities;
|
||||
}
|
||||
|
||||
public setServerCapabilities(value: data.DataProtocolServerCapabilities) {
|
||||
this._serverCapabilities = value;
|
||||
}
|
||||
|
||||
public get serverName(): string {
|
||||
return this.getSpecialTypeOptionValue(ConnectionOptionSpecialType.serverName);
|
||||
}
|
||||
|
||||
public get databaseName(): string {
|
||||
return this.getSpecialTypeOptionValue(ConnectionOptionSpecialType.databaseName);
|
||||
}
|
||||
|
||||
public get userName(): string {
|
||||
return this.getSpecialTypeOptionValue(ConnectionOptionSpecialType.userName);
|
||||
}
|
||||
|
||||
public get password(): string {
|
||||
return this.getSpecialTypeOptionValue(ConnectionOptionSpecialType.password);
|
||||
}
|
||||
|
||||
public get authenticationType(): string {
|
||||
return this.getSpecialTypeOptionValue(ConnectionOptionSpecialType.authType);
|
||||
}
|
||||
|
||||
public set serverName(value: string) {
|
||||
this.setSpecialTypeOptionName(ConnectionOptionSpecialType.serverName, value);
|
||||
}
|
||||
|
||||
public set databaseName(value: string) {
|
||||
this.setSpecialTypeOptionName(ConnectionOptionSpecialType.databaseName, value);
|
||||
}
|
||||
|
||||
public set userName(value: string) {
|
||||
this.setSpecialTypeOptionName(ConnectionOptionSpecialType.userName, value);
|
||||
}
|
||||
|
||||
public set password(value: string) {
|
||||
this.setSpecialTypeOptionName(ConnectionOptionSpecialType.password, value);
|
||||
}
|
||||
|
||||
public set authenticationType(value: string) {
|
||||
this.setSpecialTypeOptionName(ConnectionOptionSpecialType.authType, value);
|
||||
}
|
||||
|
||||
public getOptionValue(name: string): any {
|
||||
return this.options[name];
|
||||
}
|
||||
|
||||
public setOptionValue(name: string, value: any): void {
|
||||
//TODO: validate
|
||||
this.options[name] = value;
|
||||
}
|
||||
|
||||
public isPasswordRequired(): boolean {
|
||||
let optionMetadata = this._serverCapabilities.connectionProvider.options.find(
|
||||
option => option.specialValueType === ConnectionOptionSpecialType.password);
|
||||
let isPasswordRequired: boolean = optionMetadata.isRequired;
|
||||
if (this.providerName === Constants.mssqlProviderName) {
|
||||
isPasswordRequired = this.authenticationType === ProviderConnectionInfo.SqlAuthentication && optionMetadata.isRequired;
|
||||
}
|
||||
return isPasswordRequired;
|
||||
}
|
||||
|
||||
private getSpecialTypeOptionValue(type: number): string {
|
||||
let name = this.getSpecialTypeOptionName(type);
|
||||
if (name) {
|
||||
return this.options[name];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a key derived the connections options (providerName, authenticationType, serverName, databaseName, userName, groupid)
|
||||
* This key uniquely identifies a connection in a group
|
||||
* Example: "providerName:MSSQL|authenticationType:|databaseName:database|serverName:server3|userName:user|group:testid"
|
||||
*/
|
||||
public getOptionsKey(): string {
|
||||
let idNames = [];
|
||||
if (this._serverCapabilities) {
|
||||
idNames = this._serverCapabilities.connectionProvider.options.map(o => {
|
||||
if ((o.specialValueType || o.isIdentity) && o.specialValueType !== ConnectionOptionSpecialType.password) {
|
||||
return o.name;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// This should never happen but just incase the serverCapabilities was not ready at this time
|
||||
idNames = ['authenticationType', 'database', 'server', 'user'];
|
||||
}
|
||||
|
||||
idNames = idNames.filter(x => x !== undefined);
|
||||
|
||||
//Sort to make sure using names in the same order every time otherwise the ids would be different
|
||||
idNames.sort();
|
||||
|
||||
let idValues: string[] = [];
|
||||
for (var index = 0; index < idNames.length; index++) {
|
||||
let value = this.options[idNames[index]];
|
||||
value = value ? value : '';
|
||||
idValues.push(`${idNames[index]}${ProviderConnectionInfo.nameValueSeparator}${value}`);
|
||||
}
|
||||
|
||||
return ProviderConnectionInfo.ProviderPropertyName + ProviderConnectionInfo.nameValueSeparator +
|
||||
this.providerName + ProviderConnectionInfo.idSeparator + idValues.join(ProviderConnectionInfo.idSeparator);
|
||||
}
|
||||
|
||||
public static getProviderFromOptionsKey(optionsKey: string) {
|
||||
let providerId: string = '';
|
||||
if (optionsKey) {
|
||||
let ids: string[] = optionsKey.split(ProviderConnectionInfo.idSeparator);
|
||||
ids.forEach(id => {
|
||||
let idParts = id.split(ProviderConnectionInfo.nameValueSeparator);
|
||||
if (idParts.length >= 2 && idParts[0] === ProviderConnectionInfo.ProviderPropertyName) {
|
||||
providerId = idParts[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
return providerId;
|
||||
}
|
||||
|
||||
public getSpecialTypeOptionName(type: number): string {
|
||||
if (this._serverCapabilities) {
|
||||
let optionMetadata = this._serverCapabilities.connectionProvider.options.find(o => o.specialValueType === type);
|
||||
return !!optionMetadata ? optionMetadata.name : undefined;
|
||||
} else {
|
||||
return type.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public setSpecialTypeOptionName(type: number, value: string): void {
|
||||
let name = this.getSpecialTypeOptionName(type);
|
||||
if (!!name) {
|
||||
this.options[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public get authenticationTypeDisplayName(): string {
|
||||
let optionMetadata = this._serverCapabilities.connectionProvider.options.find(o => o.specialValueType === ConnectionOptionSpecialType.authType);
|
||||
let authType = this.authenticationType;
|
||||
let displayName: string = authType;
|
||||
|
||||
if (optionMetadata && optionMetadata.categoryValues) {
|
||||
optionMetadata.categoryValues.forEach(element => {
|
||||
if (element.name === authType) {
|
||||
displayName = element.displayName;
|
||||
}
|
||||
});
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public getProviderOptions(): data.ConnectionOption[] {
|
||||
return this._serverCapabilities.connectionProvider.options;
|
||||
}
|
||||
|
||||
public static get idSeparator(): string {
|
||||
return '|';
|
||||
}
|
||||
|
||||
public static get nameValueSeparator(): string {
|
||||
return ':';
|
||||
}
|
||||
|
||||
public get titleParts(): string[] {
|
||||
let parts: string[] = [];
|
||||
// Always put these three on top. TODO: maybe only for MSSQL?
|
||||
parts.push(this.serverName);
|
||||
parts.push(this.databaseName);
|
||||
parts.push(this.authenticationTypeDisplayName);
|
||||
|
||||
this._serverCapabilities.connectionProvider.options.forEach(element => {
|
||||
if (element.specialValueType !== ConnectionOptionSpecialType.serverName &&
|
||||
element.specialValueType !== ConnectionOptionSpecialType.databaseName &&
|
||||
element.specialValueType !== ConnectionOptionSpecialType.authType &&
|
||||
element.specialValueType !== ConnectionOptionSpecialType.password &&
|
||||
element.isIdentity && element.valueType === ServiceOptionType.string) {
|
||||
let value = this.getOptionValue(element.name);
|
||||
if (value) {
|
||||
parts.push(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
152
src/sql/parts/connection/common/utils.ts
Normal file
152
src/sql/parts/connection/common/utils.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IConnectionProfile } from './interfaces';
|
||||
import { ConnectionProfile } from './connectionProfile';
|
||||
import { ConnectionProfileGroup } from './connectionProfileGroup';
|
||||
|
||||
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
|
||||
const msInH = 3.6e6;
|
||||
const msInM = 60000;
|
||||
const msInS = 1000;
|
||||
export const uriPrefixes = {
|
||||
default: 'connection://',
|
||||
connection: 'connection://',
|
||||
dashboard: 'dashboard://',
|
||||
insights: 'insights://'
|
||||
};
|
||||
|
||||
|
||||
// FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export const defaultGroupId = 'C777F06B-202E-4480-B475-FA416154D458';
|
||||
export const ConnectionUriBackupIdAttributeName = 'backupId';
|
||||
export const ConnectionUriRestoreIdAttributeName = 'restoreId';
|
||||
|
||||
/**
|
||||
* Takes a string in the format of HH:MM:SS.MS and returns a number representing the time in
|
||||
* miliseconds
|
||||
* @param value The string to convert to milliseconds
|
||||
* @return False is returned if the string is an invalid format,
|
||||
* the number of milliseconds in the time string is returned otherwise.
|
||||
*/
|
||||
export function parseTimeString(value: string): number | boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
let tempVal = value.split('.');
|
||||
|
||||
if (tempVal.length === 1) {
|
||||
// Ideally would handle more cleanly than this but for now handle case where ms not set
|
||||
tempVal = [tempVal[0], '0'];
|
||||
} else if (tempVal.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let msString = tempVal[1];
|
||||
let msStringEnd = msString.length < 3 ? msString.length : 3;
|
||||
let ms = parseInt(tempVal[1].substring(0, msStringEnd), 10);
|
||||
|
||||
tempVal = tempVal[0].split(':');
|
||||
|
||||
if (tempVal.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let h = parseInt(tempVal[0], 10);
|
||||
let m = parseInt(tempVal[1], 10);
|
||||
let s = parseInt(tempVal[2], 10);
|
||||
|
||||
return ms + (h * msInH) + (m * msInM) + (s * msInS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a number of milliseconds and converts it to a string like HH:MM:SS.fff
|
||||
* @param value The number of milliseconds to convert to a timespan string
|
||||
* @returns A properly formatted timespan string.
|
||||
*/
|
||||
export function parseNumAsTimeString(value: number): string {
|
||||
let tempVal = value;
|
||||
let h = Math.floor(tempVal / msInH);
|
||||
tempVal %= msInH;
|
||||
let m = Math.floor(tempVal / msInM);
|
||||
tempVal %= msInM;
|
||||
let s = Math.floor(tempVal / msInS);
|
||||
tempVal %= msInS;
|
||||
|
||||
let hs = h < 10 ? '0' + h : '' + h;
|
||||
let ms = m < 10 ? '0' + m : '' + m;
|
||||
let ss = s < 10 ? '0' + s : '' + s;
|
||||
let mss = tempVal < 10 ? '00' + tempVal : tempVal < 100 ? '0' + tempVal : '' + tempVal;
|
||||
|
||||
let rs = hs + ':' + ms + ':' + ss;
|
||||
|
||||
return tempVal > 0 ? rs + '.' + mss : rs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts <, >, &, ", ', and any characters that are outside \u00A0 to numeric HTML entity values
|
||||
* like {
|
||||
* (Adapted from http://stackoverflow.com/a/18750001)
|
||||
* @param str String to convert
|
||||
* @return String with characters replaced.
|
||||
*/
|
||||
export function htmlEntities(str: string): string {
|
||||
return typeof (str) === 'string'
|
||||
? str.replace(/[\u00A0-\u9999<>\&"']/gim, (i) => { return `&#${i.charCodeAt(0)};`; })
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): string {
|
||||
let prefix = purpose ? uriPrefixes[purpose] : uriPrefixes.default;
|
||||
let uri = generateUriWithPrefix(connection, prefix);
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
export function getUriPrefix(ownerUri: string): string {
|
||||
let prefix: string = '';
|
||||
if (ownerUri) {
|
||||
let index = ownerUri.indexOf('://');
|
||||
if (index > 0) {
|
||||
prefix = ownerUri.substring(0, index + 3);
|
||||
} else {
|
||||
return uriPrefixes.default;
|
||||
}
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
|
||||
export function generateUriWithPrefix(connection: IConnectionProfile, prefix: string): string {
|
||||
let id = connection.getOptionsKey();
|
||||
let uri = prefix + (id ? id : connection.serverName + ':' + connection.databaseName);
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
export function findProfileInGroup(og: IConnectionProfile, groups: ConnectionProfileGroup[]): ConnectionProfile {
|
||||
for (let group of groups) {
|
||||
for (let conn of group.connections) {
|
||||
if (conn.id === og.id) {
|
||||
return conn;
|
||||
}
|
||||
}
|
||||
|
||||
if (group.hasChildren()) {
|
||||
let potentialReturn = findProfileInGroup(og, group.children);
|
||||
if (potentialReturn) {
|
||||
return potentialReturn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isMaster(profile: IConnectionProfile): boolean {
|
||||
return profile.providerName.toLowerCase() === 'mssql' && profile.databaseName.toLowerCase() === 'master';
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { AdvancedPropertiesDialog } from 'sql/parts/connection/connectionDialog/advancedPropertiesDialog';
|
||||
import { OptionsDialog } from 'sql/base/browser/ui/modal/optionsDialog';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import data = require('data');
|
||||
import { localize } from 'vs/nls';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
|
||||
export class AdvancedPropertiesController {
|
||||
private _container: HTMLElement;
|
||||
|
||||
private _advancedDialog: OptionsDialog;
|
||||
private _options: { [name: string]: any };
|
||||
|
||||
constructor(private _onCloseAdvancedProperties: () => void,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
private handleOnOk(): void {
|
||||
this._options = this._advancedDialog.optionValues;
|
||||
}
|
||||
|
||||
public showDialog(providerOptions: data.ConnectionOption[], container: HTMLElement, options: { [name: string]: any }): void {
|
||||
this._options = options;
|
||||
this._container = container;
|
||||
var serviceOptions = providerOptions.map(option => AdvancedPropertiesController.connectionOptionToServiceOption(option));
|
||||
this.advancedDialog.open(serviceOptions, this._options);
|
||||
}
|
||||
|
||||
public get advancedDialog() {
|
||||
if (!this._advancedDialog) {
|
||||
this._advancedDialog = this._instantiationService.createInstance(
|
||||
OptionsDialog, localize('connectionAdvancedProperties', 'Advanced properties'), TelemetryKeys.ConnectionAdvancedProperties, { hasBackButton: true });
|
||||
this._advancedDialog.cancelLabel = localize('discard', 'Discard');
|
||||
this._advancedDialog.onCloseEvent(() => this._onCloseAdvancedProperties());
|
||||
this._advancedDialog.onOk(() => this.handleOnOk());
|
||||
this._advancedDialog.render();
|
||||
}
|
||||
return this._advancedDialog;
|
||||
}
|
||||
|
||||
public set advancedDialog(dialog: OptionsDialog) {
|
||||
this._advancedDialog = dialog;
|
||||
}
|
||||
|
||||
public static connectionOptionToServiceOption(connectionOption: data.ConnectionOption): data.ServiceOption {
|
||||
return {
|
||||
name: connectionOption.name,
|
||||
displayName: connectionOption.displayName,
|
||||
description: connectionOption.description,
|
||||
groupName: connectionOption.groupName,
|
||||
valueType: connectionOption.valueType,
|
||||
defaultValue: connectionOption.defaultValue,
|
||||
objectType: undefined,
|
||||
categoryValues: connectionOption.categoryValues,
|
||||
isRequired: connectionOption.isRequired,
|
||||
isArray: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IConnectionManagementService, ConnectionOptionSpecialType } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { IConnectionComponentCallbacks, IConnectionComponentController, IConnectionResult } 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';
|
||||
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup';
|
||||
import * as Constants from 'sql/parts/connection/common/constants';
|
||||
import data = require('data');
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class ConnectionController implements IConnectionComponentController {
|
||||
private _container: HTMLElement;
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
private _callback: IConnectionComponentCallbacks;
|
||||
private _connectionWidget: ConnectionWidget;
|
||||
private _advancedController: AdvancedPropertiesController;
|
||||
private _model: IConnectionProfile;
|
||||
private _providerOptions: data.ConnectionOption[];
|
||||
private _providerName: string;
|
||||
|
||||
constructor(container: HTMLElement,
|
||||
connectionManagementService: IConnectionManagementService,
|
||||
sqlCapabilities: data.DataProtocolServerCapabilities,
|
||||
callback: IConnectionComponentCallbacks,
|
||||
providerName: string,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService, ) {
|
||||
this._container = container;
|
||||
this._connectionManagementService = connectionManagementService;
|
||||
this._callback = callback;
|
||||
this._providerOptions = sqlCapabilities.connectionProvider.options;
|
||||
var specialOptions = this._providerOptions.filter(
|
||||
(property) => (property.specialValueType !== null && property.specialValueType !== undefined));
|
||||
this._connectionWidget = this._instantiationService.createInstance(ConnectionWidget, specialOptions, {
|
||||
onSetConnectButton: (enable: boolean) => this._callback.onSetConnectButton(enable),
|
||||
onCreateNewServerGroup: () => this.onCreateNewServerGroup(),
|
||||
onAdvancedProperties: () => this.handleOnAdvancedProperties(),
|
||||
onSetAzureTimeOut: () => this.handleonSetAzureTimeOut()
|
||||
}, providerName);
|
||||
this._providerName = providerName;
|
||||
}
|
||||
|
||||
private onCreateNewServerGroup(): void {
|
||||
this._connectionManagementService.showCreateServerGroupDialog({
|
||||
onAddGroup: (groupName) => this._connectionWidget.updateServerGroup(this.getAllServerGroups(), groupName),
|
||||
onClose: () => this._connectionWidget.focusOnServerGroup()
|
||||
});
|
||||
}
|
||||
|
||||
private handleonSetAzureTimeOut(): void {
|
||||
var timeoutPropertyName = 'connectTimeout';
|
||||
var timeoutOption = this._model.options[timeoutPropertyName];
|
||||
if (timeoutOption === undefined || timeoutOption === null) {
|
||||
this._model.options[timeoutPropertyName] = 30;
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnAdvancedProperties(): void {
|
||||
if (!this._advancedController) {
|
||||
this._advancedController = this._instantiationService.createInstance(AdvancedPropertiesController, () => this._connectionWidget.focusOnAdvancedButton());
|
||||
}
|
||||
var advancedOption = this._providerOptions.filter(
|
||||
(property) => (property.specialValueType === undefined || property.specialValueType === null));
|
||||
this._advancedController.showDialog(advancedOption, this._container, this._model.options);
|
||||
}
|
||||
|
||||
public showUiComponent(container: HTMLElement): void {
|
||||
this._connectionWidget.createConnectionWidget(container);
|
||||
}
|
||||
|
||||
private getServerGroupHelper(group: ConnectionProfileGroup, groupNames: IConnectionProfileGroup[]): void {
|
||||
if (group) {
|
||||
if (group.fullName !== '') {
|
||||
groupNames.push(group);
|
||||
}
|
||||
if (group.hasChildren()) {
|
||||
group.children.forEach((child) => this.getServerGroupHelper(child, groupNames));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getAllServerGroups(): IConnectionProfileGroup[] {
|
||||
var connectionGroupRoot = this._connectionManagementService.getConnectionGroups();
|
||||
var connectionGroupNames: IConnectionProfileGroup[] = [];
|
||||
if (connectionGroupRoot && connectionGroupRoot.length > 0) {
|
||||
this.getServerGroupHelper(connectionGroupRoot[0], connectionGroupNames);
|
||||
}
|
||||
let defaultGroupId: string;
|
||||
if (connectionGroupRoot && connectionGroupRoot.length > 0 && ConnectionProfileGroup.isRoot(connectionGroupRoot[0].name)) {
|
||||
defaultGroupId = connectionGroupRoot[0].id;
|
||||
} else {
|
||||
defaultGroupId = Utils.defaultGroupId;
|
||||
}
|
||||
connectionGroupNames.push(Object.assign({}, this._connectionWidget.DefaultServerGroup, { id: defaultGroupId }));
|
||||
connectionGroupNames.push(this._connectionWidget.NoneServerGroup);
|
||||
return connectionGroupNames;
|
||||
}
|
||||
|
||||
public initDialog(connectionInfo: IConnectionProfile): void {
|
||||
this._connectionWidget.updateServerGroup(this.getAllServerGroups());
|
||||
this._model = connectionInfo;
|
||||
this._model.providerName = this._providerName;
|
||||
let appNameOption = this._providerOptions.find(option => option.specialValueType === ConnectionOptionSpecialType.appName);
|
||||
if (appNameOption) {
|
||||
let appNameKey = appNameOption.name;
|
||||
this._model.options[appNameKey] = Constants.applicationName;
|
||||
}
|
||||
this._connectionWidget.initDialog(this._model);
|
||||
}
|
||||
|
||||
public focusOnOpen(): void {
|
||||
this._connectionWidget.focusOnOpen();
|
||||
}
|
||||
|
||||
public validateConnection(): IConnectionResult {
|
||||
return { isValid: this._connectionWidget.connect(this._model), connection: this._model };
|
||||
}
|
||||
|
||||
public fillInConnectionInputs(connectionInfo: IConnectionProfile): void {
|
||||
this._model = connectionInfo;
|
||||
this._connectionWidget.fillInConnectionInputs(connectionInfo);
|
||||
}
|
||||
|
||||
public handleOnConnecting(): void {
|
||||
this._connectionWidget.handleOnConnecting();
|
||||
}
|
||||
|
||||
public handleResetConnection(): void {
|
||||
this._connectionWidget.handleResetConnection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {
|
||||
IConnectionDialogService, IConnectionManagementService, IErrorMessageService,
|
||||
ConnectionType, INewConnectionParams, IConnectionCompletionOptions
|
||||
} from 'sql/parts/connection/common/connectionManagement';
|
||||
import { ConnectionDialogWidget, OnShowUIResponse } from 'sql/parts/connection/connectionDialog/connectionDialogWidget';
|
||||
import { ConnectionController } from 'sql/parts/connection/connectionDialog/connectionController';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import * as Constants from 'sql/parts/connection/common/constants';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import * as data from 'data';
|
||||
|
||||
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 Severity from 'vs/base/common/severity';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
|
||||
export interface IConnectionResult {
|
||||
isValid: boolean;
|
||||
connection: IConnectionProfile;
|
||||
}
|
||||
|
||||
export interface IConnectionComponentCallbacks {
|
||||
onSetConnectButton: (enable: boolean) => void;
|
||||
onCreateNewServerGroup?: () => void;
|
||||
onAdvancedProperties?: () => void;
|
||||
onSetAzureTimeOut?: () => void;
|
||||
}
|
||||
|
||||
export interface IConnectionComponentController {
|
||||
showUiComponent(container: HTMLElement): void;
|
||||
initDialog(model: IConnectionProfile): void;
|
||||
validateConnection(): IConnectionResult;
|
||||
fillInConnectionInputs(connectionInfo: IConnectionProfile): void;
|
||||
handleOnConnecting(): void;
|
||||
handleResetConnection(): void;
|
||||
focusOnOpen(): void;
|
||||
}
|
||||
|
||||
export class ConnectionDialogService implements IConnectionDialogService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
private _container: HTMLElement;
|
||||
private _connectionDialog: ConnectionDialogWidget;
|
||||
private _connectionControllerMap: { [providerDisplayName: string]: IConnectionComponentController };
|
||||
private _model: ConnectionProfile;
|
||||
private _params: INewConnectionParams;
|
||||
private _inputModel: IConnectionProfile;
|
||||
private _capabilitiesMaps: { [providerDisplayName: string]: data.DataProtocolServerCapabilities };
|
||||
private _providerNameToDisplayNameMap: { [providerDisplayName: string]: string };
|
||||
private _providerTypes: string[];
|
||||
private _currentProviderType: string = 'Microsoft SQL Server';
|
||||
private _connecting: boolean = false;
|
||||
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
|
||||
) {
|
||||
this._capabilitiesMaps = {};
|
||||
this._providerNameToDisplayNameMap = {};
|
||||
this._connectionControllerMap = {};
|
||||
this._providerTypes = [];
|
||||
if (_capabilitiesService) {
|
||||
_capabilitiesService.onProviderRegisteredEvent((capabilities => {
|
||||
let defaultProvider = this.getDefaultProviderName();
|
||||
if (capabilities.providerName === defaultProvider) {
|
||||
this.showDialogWithModel();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultProviderName() {
|
||||
if (this._workspaceConfigurationService) {
|
||||
let defaultProvider = WorkbenchUtils.getSqlConfigValue<string>(this._workspaceConfigurationService, Constants.defaultEngine);
|
||||
if (defaultProvider
|
||||
&& this._capabilitiesMaps
|
||||
&& defaultProvider in this._capabilitiesMaps) {
|
||||
return defaultProvider;
|
||||
}
|
||||
}
|
||||
// as a fallback, default to MSSQL if the value from settings is not available
|
||||
return Constants.mssqlProviderName;
|
||||
}
|
||||
|
||||
private handleOnConnect(params: INewConnectionParams, profile?: IConnectionProfile): void {
|
||||
if (!this._connecting) {
|
||||
this._connecting = true;
|
||||
this.handleProviderOnConnecting();
|
||||
if (!profile) {
|
||||
let result = this.uiController.validateConnection();
|
||||
if (!result.isValid) {
|
||||
this._connecting = false;
|
||||
this._connectionDialog.resetConnection();
|
||||
return;
|
||||
}
|
||||
profile = result.connection;
|
||||
|
||||
// append the port to the server name for SQL Server connections
|
||||
if (this.getCurrentProviderName() === Constants.mssqlProviderName) {
|
||||
let portPropertyName: string = 'port';
|
||||
let portOption: string = profile.options[portPropertyName];
|
||||
if (portOption && portOption.indexOf(',') === -1) {
|
||||
profile.serverName = profile.serverName + ',' + portOption;
|
||||
}
|
||||
profile.options[portPropertyName] = undefined;
|
||||
}
|
||||
|
||||
// Disable password prompt during reconnect if connected with an empty password
|
||||
if (profile.password === '' && profile.savePassword === false) {
|
||||
profile.savePassword = true;
|
||||
}
|
||||
|
||||
this.handleDefaultOnConnect(params, profile);
|
||||
} else {
|
||||
this._connectionManagementService.addSavedPassword(profile).then(connectionWithPassword => {
|
||||
this.handleDefaultOnConnect(params, connectionWithPassword);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnCancel(params: INewConnectionParams): void {
|
||||
if (params && params.input && params.connectionType === ConnectionType.editor) {
|
||||
this._connectionManagementService.cancelEditorConnection(params.input);
|
||||
} else {
|
||||
this._connectionManagementService.cancelConnection(this._model);
|
||||
}
|
||||
if (params && params.input && params.input.onConnectReject) {
|
||||
params.input.onConnectReject();
|
||||
}
|
||||
this._connectionDialog.resetConnection();
|
||||
this._connecting = false;
|
||||
}
|
||||
|
||||
private handleDefaultOnConnect(params: INewConnectionParams, connection: IConnectionProfile): Thenable<void> {
|
||||
let fromEditor = params && params.connectionType === ConnectionType.editor;
|
||||
let uri: string = undefined;
|
||||
if (fromEditor && params.input) {
|
||||
uri = params.input.uri;
|
||||
}
|
||||
let options: IConnectionCompletionOptions = {
|
||||
params: params,
|
||||
saveTheConnection: !fromEditor,
|
||||
showDashboard: params.showDashboard !== undefined ? params.showDashboard : !fromEditor,
|
||||
showConnectionDialogOnError: false,
|
||||
showFirewallRuleOnError: true
|
||||
};
|
||||
|
||||
return this._connectionManagementService.connectAndSaveProfile(connection, uri, options, params.input).then(connectionResult => {
|
||||
this._connecting = false;
|
||||
if (connectionResult && connectionResult.connected) {
|
||||
this._connectionDialog.close();
|
||||
} else if (connectionResult && connectionResult.errorHandled) {
|
||||
this._connectionDialog.resetConnection();
|
||||
} else {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage);
|
||||
this._connectionDialog.resetConnection();
|
||||
}
|
||||
}).catch(err => {
|
||||
this._connecting = false;
|
||||
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, err);
|
||||
this._connectionDialog.resetConnection();
|
||||
});
|
||||
}
|
||||
|
||||
private get uiController(): IConnectionComponentController {
|
||||
// Find the provider name from the selected provider type, or throw an error if it does not correspond to a known provider
|
||||
let providerName = this.getCurrentProviderName();
|
||||
if (!providerName) {
|
||||
throw Error('Invalid provider type');
|
||||
}
|
||||
|
||||
// Set the model name, initialize the controller if needed, and return the controller
|
||||
this._model.providerName = providerName;
|
||||
if (!this._connectionControllerMap[providerName]) {
|
||||
this._connectionControllerMap[providerName] = this._instantiationService.createInstance(ConnectionController, this._container, this._connectionManagementService, this._capabilitiesMaps[providerName], {
|
||||
onSetConnectButton: (enable: boolean) => this.handleSetConnectButtonEnable(enable)
|
||||
}, providerName);
|
||||
}
|
||||
return this._connectionControllerMap[providerName];
|
||||
}
|
||||
|
||||
private handleSetConnectButtonEnable(enable: boolean): void {
|
||||
this._connectionDialog.connectButtonState = enable;
|
||||
}
|
||||
|
||||
private handleShowUiComponent(input: OnShowUIResponse) {
|
||||
this._currentProviderType = input.selectedProviderType;
|
||||
this._model = new ConnectionProfile(this._capabilitiesMaps[this.getCurrentProviderName()], this._model);
|
||||
this.uiController.showUiComponent(input.container);
|
||||
}
|
||||
|
||||
private handleInitDialog() {
|
||||
this.uiController.initDialog(this._model);
|
||||
}
|
||||
|
||||
private handleFillInConnectionInputs(connectionInfo: IConnectionProfile): void {
|
||||
this._connectionManagementService.addSavedPassword(connectionInfo).then(connectionWithPassword => {
|
||||
var model = this.createModel(connectionWithPassword);
|
||||
this._model = model;
|
||||
this.uiController.fillInConnectionInputs(model);
|
||||
});
|
||||
this._connectionDialog.updateProvider(this._providerNameToDisplayNameMap[connectionInfo.providerName]);
|
||||
}
|
||||
|
||||
private handleProviderOnResetConnection(): void {
|
||||
this.uiController.handleResetConnection();
|
||||
}
|
||||
|
||||
private handleProviderOnConnecting(): void {
|
||||
this.uiController.handleOnConnecting();
|
||||
}
|
||||
|
||||
private updateModelServerCapabilities(model: IConnectionProfile) {
|
||||
this._model = this.createModel(model);
|
||||
this._currentProviderType = this._providerNameToDisplayNameMap[this._model.providerName];
|
||||
if (this._connectionDialog) {
|
||||
this._connectionDialog.updateProvider(this._currentProviderType);
|
||||
}
|
||||
}
|
||||
|
||||
private createModel(model: IConnectionProfile): ConnectionProfile {
|
||||
let defaultProvider = this.getDefaultProviderName();
|
||||
let providerName = model ? model.providerName : defaultProvider;
|
||||
providerName = providerName ? providerName : defaultProvider;
|
||||
let serverCapabilities = this._capabilitiesMaps[providerName];
|
||||
let newProfile = new ConnectionProfile(serverCapabilities, model);
|
||||
newProfile.saveProfile = true;
|
||||
newProfile.generateNewId();
|
||||
// If connecting from a query editor set "save connection" to false
|
||||
if (this._params && this._params.input && this._params.connectionType === ConnectionType.editor) {
|
||||
newProfile.saveProfile = false;
|
||||
}
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
private cacheCapabilities(capabilities: data.DataProtocolServerCapabilities) {
|
||||
if (capabilities) {
|
||||
this._providerTypes.push(capabilities.providerDisplayName);
|
||||
this._capabilitiesMaps[capabilities.providerName] = capabilities;
|
||||
this._providerNameToDisplayNameMap[capabilities.providerName] = capabilities.providerDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
private showDialogWithModel(): TPromise<void> {
|
||||
return new TPromise<void>((resolve, reject) => {
|
||||
if (this.getDefaultProviderName() in this._capabilitiesMaps) {
|
||||
this.updateModelServerCapabilities(this._inputModel);
|
||||
|
||||
this.doShowDialog(this._params);
|
||||
}
|
||||
let none: void;
|
||||
resolve(none);
|
||||
});
|
||||
}
|
||||
|
||||
public showDialog(
|
||||
connectionManagementService: IConnectionManagementService,
|
||||
params: INewConnectionParams,
|
||||
model?: IConnectionProfile,
|
||||
error?: string): Thenable<void> {
|
||||
|
||||
this._connectionManagementService = connectionManagementService;
|
||||
this._params = params;
|
||||
this._inputModel = model;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// only create the provider maps first time the dialog gets called
|
||||
let capabilitiesPromise: Promise<void> = Promise.resolve();
|
||||
if (this._providerTypes.length === 0) {
|
||||
capabilitiesPromise = this._capabilitiesService.onCapabilitiesReady().then(() => {
|
||||
let capabilities = this._capabilitiesService.getCapabilities();
|
||||
capabilities.forEach(c => {
|
||||
this.cacheCapabilities(c);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
capabilitiesPromise.then(success => {
|
||||
this.updateModelServerCapabilities(model);
|
||||
// If connecting from a query editor set "save connection" to false
|
||||
if (params && params.input && params.connectionType === ConnectionType.editor) {
|
||||
this._model.saveProfile = false;
|
||||
}
|
||||
|
||||
resolve(this.showDialogWithModel().then(() => {
|
||||
if (error && error !== '') {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._connectionErrorTitle, error);
|
||||
}
|
||||
}));
|
||||
}, err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
private doShowDialog(params: INewConnectionParams): TPromise<void> {
|
||||
if (!this._connectionDialog) {
|
||||
let container = withElementById(this._partService.getWorkbenchElementId()).getHTMLElement().parentElement;
|
||||
this._container = container;
|
||||
this._connectionDialog = this._instantiationService.createInstance(ConnectionDialogWidget, this._providerTypes, this._providerNameToDisplayNameMap[this._model.providerName]);
|
||||
this._connectionDialog.onCancel(() => this.handleOnCancel(this._connectionDialog.newConnectionParams));
|
||||
this._connectionDialog.onConnect((profile) => this.handleOnConnect(this._connectionDialog.newConnectionParams, profile));
|
||||
this._connectionDialog.onShowUiComponent((input) => this.handleShowUiComponent(input));
|
||||
this._connectionDialog.onInitDialog(() => this.handleInitDialog());
|
||||
this._connectionDialog.onFillinConnectionInputs((input) => this.handleFillInConnectionInputs(input));
|
||||
this._connectionDialog.onResetConnection(() => this.handleProviderOnResetConnection());
|
||||
this._connectionDialog.render();
|
||||
}
|
||||
this._connectionDialog.newConnectionParams = params;
|
||||
|
||||
return new TPromise<void>(() => {
|
||||
this._connectionDialog.open(this._connectionManagementService.getRecentConnections().length > 0);
|
||||
this.uiController.focusOnOpen();
|
||||
});
|
||||
}
|
||||
|
||||
private getCurrentProviderName(): string {
|
||||
return 'MSSQL';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./media/connectionDialog';
|
||||
|
||||
import { attachModalDialogStyler } from 'sql/common/theme/styler';
|
||||
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';
|
||||
import { IConnectionManagementService, INewConnectionParams } from 'sql/parts/connection/common/connectionManagement';
|
||||
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
|
||||
import { TreeCreationUtils } from 'sql/parts/registeredServer/viewlet/treeCreationUtils';
|
||||
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 { 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';
|
||||
|
||||
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 _dividerBuilder: Builder;
|
||||
private _connectButton: Button;
|
||||
private _closeButton: Button;
|
||||
private _newConnectionParams: INewConnectionParams;
|
||||
private _recentConnectionTree: ITree;
|
||||
private $connectionUIContainer: Builder;
|
||||
|
||||
private _onInitDialog = new Emitter<void>();
|
||||
public onInitDialog: Event<void> = this._onInitDialog.event;
|
||||
|
||||
private _onCancel = new Emitter<void>();
|
||||
public onCancel: Event<void> = this._onCancel.event;
|
||||
|
||||
private _onConnect = new Emitter<IConnectionProfile>();
|
||||
public onConnect: Event<IConnectionProfile> = this._onConnect.event;
|
||||
|
||||
private _onShowUiComponent = new Emitter<OnShowUIResponse>();
|
||||
public onShowUiComponent: Event<OnShowUIResponse> = this._onShowUiComponent.event;
|
||||
|
||||
private _onFillinConnectionInputs = new Emitter<IConnectionProfile>();
|
||||
public onFillinConnectionInputs: Event<IConnectionProfile> = this._onFillinConnectionInputs.event;
|
||||
|
||||
private _onResetConnection = new Emitter<void>();
|
||||
public onResetConnection: Event<void> = this._onResetConnection.event;
|
||||
|
||||
constructor(
|
||||
private providerTypeOptions: string[],
|
||||
private selectedProviderType: string,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
|
||||
@IPartService _partService: IPartService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(localize('connection', 'Connection'), TelemetryKeys.Connection, _partService, telemetryService, contextKeyService, { hasSpinner: true, hasErrors: true });
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
this._bodyBuilder = new Builder(container);
|
||||
|
||||
this._bodyBuilder.div({ class: 'connection-recent', id: 'recentConnection' }, (builder) => {
|
||||
this._recentConnectionBuilder = new Builder(builder.getHTMLElement());
|
||||
this.createRecentConnections();
|
||||
this._recentConnectionBuilder.hide();
|
||||
});
|
||||
|
||||
this._bodyBuilder.div({ class: 'Connection-divider' }, (dividerContainer) => {
|
||||
this._dividerBuilder = dividerContainer;
|
||||
});
|
||||
|
||||
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');
|
||||
modelTableContent.element('table', { class: 'connection-table-content' }, (tableContainer) => {
|
||||
// DialogHelper.appendInputSelectBox(
|
||||
// DialogHelper.appendRow(tableContainer, connectTypeLabel, 'connection-label', 'connection-input'), this._providerTypeSelectBox);
|
||||
});
|
||||
});
|
||||
|
||||
this.$connectionUIContainer = $('.connection-provider-info#connectionProviderInfo');
|
||||
this.$connectionUIContainer.appendTo(this._bodyBuilder);
|
||||
|
||||
let self = this;
|
||||
this._register(self._themeService.onDidColorThemeChange(e => self.updateTheme(e)));
|
||||
self.updateTheme(self._themeService.getColorTheme());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the connection flyout
|
||||
*/
|
||||
public render() {
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
let connectLabel = localize('connect', 'Connect');
|
||||
let cancelLabel = localize('cancel', 'Cancel');
|
||||
this._connectButton = this.addFooterButton(connectLabel, () => this.connect());
|
||||
this._connectButton.enabled = false;
|
||||
this._closeButton = this.addFooterButton(cancelLabel, () => this.cancel());
|
||||
this.registerListeners();
|
||||
this.onProviderTypeSelected('MSSQL');
|
||||
}
|
||||
|
||||
// Update theming that is specific to connection flyout body
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let borderColor = theme.getColor(contrastBorder);
|
||||
let border = borderColor ? borderColor.toString() : null;
|
||||
if (this._dividerBuilder) {
|
||||
this._dividerBuilder.style('border-top-width', border ? '1px' : null);
|
||||
this._dividerBuilder.style('border-top-style', border ? 'solid' : null);
|
||||
this._dividerBuilder.style('border-top-color', border);
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(styler.attachButtonStyler(this._connectButton, this._themeService));
|
||||
this._register(styler.attachButtonStyler(this._closeButton, this._themeService));
|
||||
|
||||
}
|
||||
|
||||
private onProviderTypeSelected(selectedProviderType: string) {
|
||||
// Show connection form based on server type
|
||||
this.$connectionUIContainer.empty();
|
||||
this._onShowUiComponent.fire({ selectedProviderType: selectedProviderType, container: this.$connectionUIContainer.getHTMLElement() });
|
||||
this.initDialog();
|
||||
}
|
||||
|
||||
private connect(element?: IConnectionProfile): void {
|
||||
if (this._connectButton.enabled) {
|
||||
this._connectButton.enabled = false;
|
||||
this.showSpinner();
|
||||
this._onConnect.fire(element);
|
||||
}
|
||||
}
|
||||
|
||||
/* Overwrite espace key behavior */
|
||||
protected onClose(e: StandardKeyboardEvent) {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept(e: StandardKeyboardEvent) {
|
||||
if (!e.target.classList.contains('monaco-tree')) {
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
this._onCancel.fire();
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.resetConnection();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private createRecentConnections() {
|
||||
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: 'server-explorer-viewlet' }, (divContainer: Builder) => {
|
||||
divContainer.div({ class: 'explorer-servers' }, (treeContainer: Builder) => {
|
||||
let leftClick = (element: any, eventish: ICancelableEvent, origin: string) => {
|
||||
// element will be a server group if the tree is clicked rather than a item
|
||||
if (element instanceof ConnectionProfile) {
|
||||
this.onRecentConnectionClick({ payload: { origin: origin, originalEvent: eventish } }, element);
|
||||
}
|
||||
|
||||
};
|
||||
let controller = new TreeController(leftClick);
|
||||
this._recentConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer.getHTMLElement(), this._instantiationService, controller);
|
||||
|
||||
// Theme styler
|
||||
this._register(styler.attachListStyler(this._recentConnectionTree, this._themeService));
|
||||
divContainer.append(this._recentConnectionTree.getHTMLElement());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
if (isDoubleClick) {
|
||||
this.connect(element);
|
||||
} else {
|
||||
if (element) {
|
||||
this._onFillinConnectionInputs.fire(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the flyout dialog
|
||||
* @param recentConnections Are there recent connections that should be shown
|
||||
*/
|
||||
public open(recentConnections: boolean) {
|
||||
this.show();
|
||||
if (recentConnections) {
|
||||
this._recentConnectionBuilder.show();
|
||||
TreeUpdateUtils.structuralTreeUpdate(this._recentConnectionTree, 'recent', this._connectionManagementService);
|
||||
// call layout with view height
|
||||
this.layout();
|
||||
} else {
|
||||
this._recentConnectionBuilder.hide();
|
||||
}
|
||||
|
||||
this.initDialog();
|
||||
}
|
||||
|
||||
protected layout(height?: number): void {
|
||||
// Height is the overall height. Since we're laying out a specific component, always get its actual height
|
||||
this._recentConnectionTree.layout(DOM.getTotalHeight(this._recentConnectionTree.getHTMLElement()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state of the connect button
|
||||
* @param enabled The state to set the the button
|
||||
*/
|
||||
public set connectButtonState(enabled: boolean) {
|
||||
this._connectButton.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connect button state
|
||||
*/
|
||||
public get connectButtonState(): boolean {
|
||||
return this._connectButton.enabled;
|
||||
}
|
||||
|
||||
private initDialog(): void {
|
||||
super.setError('');
|
||||
this.hideSpinner();
|
||||
this._onInitDialog.fire();
|
||||
}
|
||||
|
||||
public resetConnection(): void {
|
||||
this.hideSpinner();
|
||||
this._connectButton.enabled = true;
|
||||
this._onResetConnection.fire();
|
||||
}
|
||||
|
||||
public get newConnectionParams(): INewConnectionParams {
|
||||
return this._newConnectionParams;
|
||||
}
|
||||
|
||||
public set newConnectionParams(params: INewConnectionParams) {
|
||||
this._newConnectionParams = params;
|
||||
}
|
||||
|
||||
public updateProvider(displayName: string) {
|
||||
this.onProviderTypeSelected('MSSQL');
|
||||
}
|
||||
}
|
||||
482
src/sql/parts/connection/connectionDialog/connectionWidget.ts
Normal file
482
src/sql/parts/connection/connectionDialog/connectionWidget.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/sqlConnection';
|
||||
import { Builder, $ } from 'vs/base/browser/builder';
|
||||
import { Button } from 'vs/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 { 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';
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { ConnectionOptionSpecialType } from 'sql/parts/connection/common/connectionManagement';
|
||||
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 * as DOM from 'vs/base/browser/dom';
|
||||
import data = require('data');
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export class ConnectionWidget {
|
||||
private _builder: Builder;
|
||||
private _serverGroupSelectBox: SelectBox;
|
||||
private _previousGroupOption: string;
|
||||
private _serverGroupOptions: IConnectionProfileGroup[];
|
||||
private _serverNameInputBox: InputBox;
|
||||
private _databaseNameInputBox: InputBox;
|
||||
private _userNameInputBox: InputBox;
|
||||
private _passwordInputBox: InputBox;
|
||||
private _rememberPasswordCheckBox: Checkbox;
|
||||
private _advancedButton: Button;
|
||||
private _callbacks: IConnectionComponentCallbacks;
|
||||
private _authTypeSelectBox: SelectBox;
|
||||
private _toDispose: lifecycle.IDisposable[];
|
||||
private _optionsMaps: { [optionType: number]: data.ConnectionOption };
|
||||
private _tableContainer: Builder;
|
||||
private _providerName: string;
|
||||
private _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
|
||||
[Constants.mssqlProviderName]: [new AuthenticationType('Integrated', false), new AuthenticationType('SqlLogin', true)]
|
||||
};
|
||||
private _saveProfile: boolean;
|
||||
public DefaultServerGroup: IConnectionProfileGroup = {
|
||||
id: '',
|
||||
name: localize('defaultServerGroup', '<Default>'),
|
||||
parentId: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
private _addNewServerGroup = {
|
||||
id: '',
|
||||
name: localize('addNewServerGroup', 'Add new group...'),
|
||||
parentId: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
public NoneServerGroup: IConnectionProfileGroup = {
|
||||
id: '',
|
||||
name: localize('noneServerGroup', '<None>'),
|
||||
parentId: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
constructor(options: data.ConnectionOption[],
|
||||
callbacks: IConnectionComponentCallbacks,
|
||||
providerName: string,
|
||||
@IThemeService private _themeService: IThemeService,
|
||||
@IContextViewService private _contextViewService: IContextViewService) {
|
||||
this._callbacks = callbacks;
|
||||
this._toDispose = [];
|
||||
this._optionsMaps = {};
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var option = options[i];
|
||||
this._optionsMaps[option.specialValueType] = option;
|
||||
}
|
||||
|
||||
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||
this._authTypeSelectBox = authTypeOption ? new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue) : undefined;
|
||||
this._providerName = providerName;
|
||||
}
|
||||
|
||||
public createConnectionWidget(container: HTMLElement): void {
|
||||
this._serverGroupOptions = [this.DefaultServerGroup];
|
||||
this._serverGroupSelectBox = new SelectBox(this._serverGroupOptions.map(g => g.name), this.DefaultServerGroup.name);
|
||||
this._previousGroupOption = this._serverGroupSelectBox.value;
|
||||
this._builder = $().div({ class: 'connection-table' }, (modelTableContent) => {
|
||||
modelTableContent.element('table', { class: 'connection-table-content' }, (tableContainer) => {
|
||||
this._tableContainer = tableContainer;
|
||||
});
|
||||
});
|
||||
this.fillInConnectionForm();
|
||||
this.registerListeners();
|
||||
if (this._authTypeSelectBox) {
|
||||
this.onAuthTypeSelected(this._authTypeSelectBox.value);
|
||||
}
|
||||
DOM.append(container, this._builder.getHTMLElement());
|
||||
}
|
||||
|
||||
private fillInConnectionForm(): void {
|
||||
let errorMessage = localize('missingRequireField', ' is required.');
|
||||
|
||||
let serverNameOption = this._optionsMaps[ConnectionOptionSpecialType.serverName];
|
||||
let serverNameBuilder = DialogHelper.appendRow(this._tableContainer, serverNameOption.displayName, 'connection-label', 'connection-input');
|
||||
this._serverNameInputBox = new InputBox(serverNameBuilder.getHTMLElement(), this._contextViewService, {
|
||||
validationOptions: {
|
||||
validation: (value: string) => !value ? ({ type: MessageType.ERROR, content: serverNameOption.displayName + errorMessage }) : null
|
||||
},
|
||||
});
|
||||
|
||||
if (this._optionsMaps[ConnectionOptionSpecialType.authType]) {
|
||||
let authTypeBuilder = DialogHelper.appendRow(this._tableContainer, this._optionsMaps[ConnectionOptionSpecialType.authType].displayName, 'connection-label', 'connection-input');
|
||||
DialogHelper.appendInputSelectBox(authTypeBuilder, this._authTypeSelectBox);
|
||||
}
|
||||
|
||||
let self = this;
|
||||
let userNameOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
|
||||
let userNameBuilder = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input');
|
||||
this._userNameInputBox = new InputBox(userNameBuilder.getHTMLElement(), this._contextViewService, {
|
||||
validationOptions: {
|
||||
validation: (value: string) => self.validateUsername(value, userNameOption.isRequired) ? ({ type: MessageType.ERROR, content: userNameOption.displayName + errorMessage }) : null
|
||||
}
|
||||
});
|
||||
|
||||
let passwordOption = this._optionsMaps[ConnectionOptionSpecialType.password];
|
||||
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';
|
||||
|
||||
let rememberPasswordLabel = localize('rememberPassword', 'Remember password');
|
||||
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', false);
|
||||
|
||||
let databaseOption = this._optionsMaps[ConnectionOptionSpecialType.databaseName];
|
||||
let databaseNameBuilder = DialogHelper.appendRow(this._tableContainer, databaseOption.displayName, 'connection-label', 'connection-input');
|
||||
this._databaseNameInputBox = new InputBox(databaseNameBuilder.getHTMLElement(), this._contextViewService, {
|
||||
validationOptions: {
|
||||
validation: (value: string) => (!value && databaseOption.isRequired) ? ({ type: MessageType.ERROR, content: databaseOption.displayName + errorMessage }) : null
|
||||
},
|
||||
placeholder: (databaseOption.defaultValue || '')
|
||||
});
|
||||
|
||||
let serverGroupLabel = localize('serverGroup', 'Server group');
|
||||
let serverGroupBuilder = DialogHelper.appendRow(this._tableContainer, serverGroupLabel, 'connection-label', 'connection-input');
|
||||
DialogHelper.appendInputSelectBox(serverGroupBuilder, this._serverGroupSelectBox);
|
||||
|
||||
let AdvancedLabel = localize('advanced', 'Advanced...');
|
||||
this._advancedButton = this.createAdvancedButton(this._tableContainer, AdvancedLabel);
|
||||
}
|
||||
|
||||
private validateUsername(value: string, isOptionRequired: boolean): boolean {
|
||||
let currentAuthType = this._authTypeSelectBox ? this.getMatchingAuthType(this._authTypeSelectBox.value) : undefined;
|
||||
if (!currentAuthType || currentAuthType.showUsernameAndPassword) {
|
||||
if (!value && isOptionRequired) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private createAdvancedButton(container: Builder, title: string): Button {
|
||||
let button;
|
||||
container.element('tr', {}, (rowContainer) => {
|
||||
rowContainer.element('td');
|
||||
rowContainer.element('td', { align: 'right' }, (cellContainer) => {
|
||||
cellContainer.div({ class: 'advanced-button' }, (divContainer) => {
|
||||
button = new Button(divContainer);
|
||||
button.label = title;
|
||||
button.addListener('click', () => {
|
||||
//open advanced page
|
||||
this._callbacks.onAdvancedProperties();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private appendCheckbox(container: Builder, label: string, checkboxClass: string, cellContainerClass: string, isChecked: boolean): Checkbox {
|
||||
let checkbox: Checkbox;
|
||||
container.element('tr', {}, (rowContainer) => {
|
||||
rowContainer.element('td');
|
||||
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
|
||||
checkbox = new Checkbox(inputCellContainer.getHTMLElement(), { label, checked: isChecked });
|
||||
});
|
||||
});
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._toDispose.push(attachInputBoxStyler(this._serverNameInputBox, this._themeService));
|
||||
this._toDispose.push(attachInputBoxStyler(this._databaseNameInputBox, this._themeService));
|
||||
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));
|
||||
|
||||
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._toDispose.push(this._serverGroupSelectBox.onDidSelect(selectedGroup => {
|
||||
this.onGroupSelected(selectedGroup.selected);
|
||||
}));
|
||||
|
||||
this._toDispose.push(this._serverNameInputBox.onDidChange(serverName => {
|
||||
this.serverNameChanged(serverName);
|
||||
}));
|
||||
}
|
||||
|
||||
private onGroupSelected(selectedGroup: string) {
|
||||
if (selectedGroup === this._addNewServerGroup.name) {
|
||||
// Select previous non-AddGroup option in case AddServerGroup dialog is cancelled
|
||||
this._serverGroupSelectBox.selectWithOptionName(this._previousGroupOption);
|
||||
this._callbacks.onCreateNewServerGroup();
|
||||
} else {
|
||||
this._previousGroupOption = selectedGroup;
|
||||
}
|
||||
}
|
||||
|
||||
private onAuthTypeSelected(selectedAuthType: string) {
|
||||
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
|
||||
if (!currentAuthType.showUsernameAndPassword) {
|
||||
this._userNameInputBox.disable();
|
||||
this._passwordInputBox.disable();
|
||||
this._userNameInputBox.hideMessage();
|
||||
this._passwordInputBox.hideMessage();
|
||||
this._userNameInputBox.value = '';
|
||||
this._passwordInputBox.value = '';
|
||||
|
||||
this._rememberPasswordCheckBox.checked = false;
|
||||
this._rememberPasswordCheckBox.enabled = false;
|
||||
} else {
|
||||
this._userNameInputBox.enable();
|
||||
this._passwordInputBox.enable();
|
||||
this._rememberPasswordCheckBox.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private serverNameChanged(serverName: string) {
|
||||
this._callbacks.onSetConnectButton(!!serverName);
|
||||
if (serverName.toLocaleLowerCase().includes('database.windows.net')) {
|
||||
this._callbacks.onSetAzureTimeOut();
|
||||
}
|
||||
}
|
||||
|
||||
public focusOnAdvancedButton() {
|
||||
this._advancedButton.focus();
|
||||
}
|
||||
|
||||
public focusOnServerGroup() {
|
||||
this._serverGroupSelectBox.focus();
|
||||
}
|
||||
|
||||
public updateServerGroup(connectionGroups: IConnectionProfileGroup[], groupName?: string) {
|
||||
this._serverGroupOptions = connectionGroups;
|
||||
this._serverGroupOptions.push(this._addNewServerGroup);
|
||||
this._serverGroupSelectBox.setOptions(this._serverGroupOptions.map(g => g.name));
|
||||
if (groupName) {
|
||||
this._serverGroupSelectBox.selectWithOptionName(groupName);
|
||||
this._previousGroupOption = this._serverGroupSelectBox.value;
|
||||
}
|
||||
}
|
||||
|
||||
public initDialog(connectionInfo: IConnectionProfile): void {
|
||||
this.fillInConnectionInputs(connectionInfo);
|
||||
}
|
||||
|
||||
public focusOnOpen(): void {
|
||||
this._serverNameInputBox.focus();
|
||||
}
|
||||
|
||||
private getModelValue(value: string): string {
|
||||
return value ? value : '';
|
||||
}
|
||||
|
||||
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._saveProfile = connectionInfo.saveProfile;
|
||||
let groupName: string;
|
||||
if (this._saveProfile) {
|
||||
if (!connectionInfo.groupFullName) {
|
||||
groupName = this.DefaultServerGroup.name;
|
||||
} else {
|
||||
groupName = connectionInfo.groupFullName.replace('root/', '');
|
||||
}
|
||||
} else {
|
||||
groupName = this.NoneServerGroup.name;
|
||||
}
|
||||
this._serverGroupSelectBox.selectWithOptionName(groupName);
|
||||
this._previousGroupOption = this._serverGroupSelectBox.value;
|
||||
|
||||
// To handle the empty password case
|
||||
if (this.getModelValue(connectionInfo.password) === '') {
|
||||
this._rememberPasswordCheckBox.checked = false;
|
||||
} else {
|
||||
this._rememberPasswordCheckBox.checked = connectionInfo.savePassword;
|
||||
}
|
||||
|
||||
if (connectionInfo.authenticationType !== null && connectionInfo.authenticationType !== undefined) {
|
||||
var authTypeDisplayName = this.getAuthTypeDisplayName(connectionInfo.authenticationType);
|
||||
this._authTypeSelectBox.selectWithOptionName(authTypeDisplayName);
|
||||
}
|
||||
|
||||
if (this._authTypeSelectBox) {
|
||||
this.onAuthTypeSelected(this._authTypeSelectBox.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getAuthTypeDisplayName(authTypeName: string) {
|
||||
var displayName: string;
|
||||
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||
authTypeOption.categoryValues.forEach(c => {
|
||||
if (c.name === authTypeName) {
|
||||
displayName = c.displayName;
|
||||
}
|
||||
});
|
||||
return displayName;
|
||||
}
|
||||
|
||||
private getAuthTypeName(authTypeDisplayName: string) {
|
||||
var authTypeName: string;
|
||||
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||
authTypeOption.categoryValues.forEach(c => {
|
||||
if (c.displayName === authTypeDisplayName) {
|
||||
authTypeName = c.name;
|
||||
}
|
||||
});
|
||||
return authTypeName;
|
||||
}
|
||||
|
||||
public handleOnConnecting(): void {
|
||||
this._advancedButton.enabled = false;
|
||||
|
||||
this._serverGroupSelectBox.disable();
|
||||
this._serverNameInputBox.disable();
|
||||
this._databaseNameInputBox.disable();
|
||||
this._userNameInputBox.disable();
|
||||
this._passwordInputBox.disable();
|
||||
this._rememberPasswordCheckBox.enabled = false;
|
||||
if (this._authTypeSelectBox) {
|
||||
this._authTypeSelectBox.disable();
|
||||
}
|
||||
}
|
||||
|
||||
public handleResetConnection(): void {
|
||||
this._advancedButton.enabled = true;
|
||||
|
||||
this._serverGroupSelectBox.enable();
|
||||
this._serverNameInputBox.enable();
|
||||
this._databaseNameInputBox.enable();
|
||||
let currentAuthType: AuthenticationType = undefined;
|
||||
if (this._authTypeSelectBox) {
|
||||
this._authTypeSelectBox.enable();
|
||||
currentAuthType = this.getMatchingAuthType(this._authTypeSelectBox.value);
|
||||
}
|
||||
|
||||
if (!currentAuthType || currentAuthType.showUsernameAndPassword) {
|
||||
this._userNameInputBox.enable();
|
||||
this._passwordInputBox.enable();
|
||||
this._rememberPasswordCheckBox.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public get serverName(): string {
|
||||
return this._serverNameInputBox.value;
|
||||
}
|
||||
|
||||
public get databaseName(): string {
|
||||
return this._databaseNameInputBox.value;
|
||||
}
|
||||
|
||||
public get userName(): string {
|
||||
return this._userNameInputBox.value;
|
||||
}
|
||||
|
||||
public get password(): string {
|
||||
return this._passwordInputBox.value;
|
||||
}
|
||||
|
||||
public get authenticationType(): string {
|
||||
return this._authTypeSelectBox ? this.getAuthTypeName(this._authTypeSelectBox.value) : undefined;
|
||||
}
|
||||
|
||||
private validateInputs(): boolean {
|
||||
let isFocused = false;
|
||||
let validateServerName = this._serverNameInputBox.validate();
|
||||
if (!validateServerName) {
|
||||
this._serverNameInputBox.focus();
|
||||
isFocused = true;
|
||||
}
|
||||
let validateUserName = this._userNameInputBox.validate();
|
||||
if (!validateUserName && !isFocused) {
|
||||
this._userNameInputBox.focus();
|
||||
isFocused = true;
|
||||
}
|
||||
let validatePassword = this._passwordInputBox.validate();
|
||||
if (!validatePassword && !isFocused) {
|
||||
this._passwordInputBox.focus();
|
||||
isFocused = true;
|
||||
}
|
||||
let validateDatabaseName = this._databaseNameInputBox.validate();
|
||||
if (!validateDatabaseName && !isFocused) {
|
||||
this._databaseNameInputBox.focus();
|
||||
}
|
||||
return validateServerName && validateUserName && validatePassword && validateDatabaseName;
|
||||
}
|
||||
|
||||
public connect(model: IConnectionProfile): boolean {
|
||||
let validInputs = this.validateInputs();
|
||||
if (validInputs) {
|
||||
model.serverName = this.serverName;
|
||||
model.databaseName = this.databaseName;
|
||||
model.userName = this.userName;
|
||||
model.password = this.password;
|
||||
model.authenticationType = this.authenticationType;
|
||||
model.savePassword = this._rememberPasswordCheckBox.checked;
|
||||
if (this._serverGroupSelectBox.value === this.DefaultServerGroup.name) {
|
||||
model.groupFullName = '';
|
||||
model.saveProfile = true;
|
||||
model.groupId = this.findGroupId(model.groupFullName);
|
||||
} else if (this._serverGroupSelectBox.value === this.NoneServerGroup.name) {
|
||||
model.groupFullName = '';
|
||||
model.saveProfile = false;
|
||||
} else if (this._serverGroupSelectBox.value !== this._addNewServerGroup.name) {
|
||||
model.groupFullName = this._serverGroupSelectBox.value;
|
||||
model.saveProfile = true;
|
||||
model.groupId = this.findGroupId(model.groupFullName);
|
||||
}
|
||||
}
|
||||
return validInputs;
|
||||
}
|
||||
|
||||
private findGroupId(groupFullName: string): string {
|
||||
let group: IConnectionProfileGroup;
|
||||
if (ConnectionProfileGroup.isRoot(groupFullName)) {
|
||||
group = this._serverGroupOptions.find(g => ConnectionProfileGroup.isRoot(g.name));
|
||||
if (group === undefined) {
|
||||
group = this._serverGroupOptions.find(g => g.name === this.DefaultServerGroup.name);
|
||||
}
|
||||
} else {
|
||||
group = this._serverGroupOptions.find(g => g.name === groupFullName);
|
||||
}
|
||||
return group ? group.id : undefined;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._toDispose = lifecycle.dispose(this._toDispose);
|
||||
}
|
||||
|
||||
private getMatchingAuthType(displayName: string): AuthenticationType {
|
||||
return this._authTypeMap[this._providerName].find(authType => this.getAuthTypeDisplayName(authType.name) === displayName);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationType {
|
||||
public name: string;
|
||||
public showUsernameAndPassword: boolean;
|
||||
|
||||
constructor(name: string, showUsernameAndPassword: boolean) {
|
||||
this.name = name;
|
||||
this.showUsernameAndPassword = showUsernameAndPassword;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.connection-label {
|
||||
width: 80px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.connection-input {
|
||||
padding-right:8px;
|
||||
width: 200px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.connection-recent {
|
||||
margin: 15px;
|
||||
height: calc(100% - 400px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.connection-history-label {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.server-name-label {
|
||||
font-size: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.connection-provider-info {
|
||||
overflow-y: hidden;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.connection-recent-content {
|
||||
height: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.connection-table-content {
|
||||
width:100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.connection-type {
|
||||
margin: 15px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.advanced-button {
|
||||
width: 100px;
|
||||
padding-right:8px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
29
src/sql/parts/dashboard/common/actions.ts
Normal file
29
src/sql/parts/dashboard/common/actions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
export class RefreshWidgetAction extends Action {
|
||||
|
||||
public static ID = 'refreshWidget';
|
||||
public static LABEL = nls.localize('refreshWidget', 'Refresh');
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
private refreshFn: () => void
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(): TPromise<boolean> {
|
||||
try {
|
||||
this.refreshFn();
|
||||
return TPromise.as(true);
|
||||
} catch (e) {
|
||||
return TPromise.as(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/sql/parts/dashboard/common/componentHost.directive.ts
Normal file
13
src/sql/parts/dashboard/common/componentHost.directive.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Directive, ViewContainerRef, Inject, forwardRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[component-host]',
|
||||
})
|
||||
export class ComponentHostDirective {
|
||||
constructor( @Inject(forwardRef(() => ViewContainerRef)) public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
||||
16
src/sql/parts/dashboard/common/dashboardPage.component.html
Normal file
16
src/sql/parts/dashboard/common/dashboardPage.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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>
|
||||
</div>
|
||||
300
src/sql/parts/dashboard/common/dashboardPage.component.ts
Normal file
300
src/sql/parts/dashboard/common/dashboardPage.component.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { 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 { 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 { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a number.
|
||||
*/
|
||||
function isNumberArray(value: any): value is number[] {
|
||||
return types.isArray(value) && (<any[]>value).every(elem => types.isNumber(elem));
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-page',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardPage.component.html')),
|
||||
host: {
|
||||
class: 'dashboard-page'
|
||||
}
|
||||
})
|
||||
export abstract class DashboardPage {
|
||||
|
||||
protected SKELETON_WIDTH = 5;
|
||||
protected widgets: Array<WidgetConfig> = [];
|
||||
protected gridConfig: NgGridConfig = {
|
||||
'margins': [10], // The size of the margins of each item. Supports up to four values in the same way as CSS margins. Can be updated using setMargins()
|
||||
'draggable': false, // Whether the items can be dragged. Can be updated using enableDrag()/disableDrag()
|
||||
'resizable': false, // Whether the items can be resized. Can be updated using enableResize()/disableResize()
|
||||
'max_cols': this.SKELETON_WIDTH, // The maximum number of columns allowed. Set to 0 for infinite. Cannot be used with max_rows
|
||||
'max_rows': 0, // The maximum number of rows allowed. Set to 0 for infinite. Cannot be used with max_cols
|
||||
'visible_cols': 0, // The number of columns shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_cols
|
||||
'visible_rows': 0, // The number of rows shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_rows
|
||||
'min_cols': 0, // The minimum number of columns allowed. Can be any number greater than or equal to 1.
|
||||
'min_rows': 0, // The minimum number of rows allowed. Can be any number greater than or equal to 1.
|
||||
'col_width': 250, // The width of each column
|
||||
'row_height': 250, // The height of each row
|
||||
'cascade': 'left', // The direction to cascade grid items ('up', 'right', 'down', 'left')
|
||||
'min_width': 100, // The minimum width of an item. If greater than col_width, this will update the value of min_cols
|
||||
'min_height': 100, // The minimum height of an item. If greater than row_height, this will update the value of min_rows
|
||||
'fix_to_grid': false, // Fix all item movements to the grid
|
||||
'auto_style': true, // Automatically add required element styles at run-time
|
||||
'auto_resize': false, // Automatically set col_width/row_height so that max_cols/max_rows fills the screen. Only has effect is max_cols or max_rows is set
|
||||
'maintain_ratio': false, // Attempts to maintain aspect ratio based on the colWidth/rowHeight values set in the config
|
||||
'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;
|
||||
|
||||
@ViewChild('propertyContainer', { read: ElementRef }) private propertyContainer: ElementRef;
|
||||
@ViewChild('properties') private _properties: DashboardWidgetWrapper;
|
||||
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
|
||||
|
||||
// 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
|
||||
];
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) protected dashboardService: DashboardServiceInterface
|
||||
) { }
|
||||
|
||||
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 properties = this.getProperties();
|
||||
this._configModifiers.forEach((cb) => {
|
||||
tempWidgets = cb.apply(this, [tempWidgets]);
|
||||
properties = properties ? cb.apply(this, [properties]) : undefined;
|
||||
});
|
||||
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);
|
||||
});
|
||||
self.updateTheme(self.dashboardService.themeService.getColorTheme());
|
||||
|
||||
}
|
||||
|
||||
protected baseDestroy(): void {
|
||||
if (this._themeDispose) {
|
||||
this._themeDispose.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract propertiesWidget: WidgetConfig;
|
||||
protected abstract get context(): string;
|
||||
|
||||
/**
|
||||
* Returns a filtered version of the widgets passed based on edition and provider
|
||||
* @param config widgets to filter
|
||||
*/
|
||||
private filterWidgets(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let connectionInfo: ConnectionManagementInfo = this.dashboardService.connectionManagementService.connectionInfo;
|
||||
let edition = connectionInfo.serverInfo.engineEditionId;
|
||||
let provider = connectionInfo.providerId;
|
||||
|
||||
// filter by provider
|
||||
return config.filter((item) => {
|
||||
return this.stringCompare(item.provider, provider);
|
||||
}).filter((item) => {
|
||||
if (item.edition) {
|
||||
if (edition) {
|
||||
return this.stringCompare(isNumberArray(item.edition) ? item.edition.map(item => item.toString()) : item.edition.toString(), edition.toString());
|
||||
} else {
|
||||
this.dashboardService.messageService.show(Severity.Warning, nls.localize('providerMissingEdition', 'Widget filters based on edition, but the provider does not have an edition'));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a compare against the val passed in and the compare string
|
||||
* @param val string or array of strings to compare the compare value to; if array, it will compare each val in the array
|
||||
* @param compare value to compare to
|
||||
*/
|
||||
private stringCompare(val: string | Array<string>, compare: string): boolean {
|
||||
if (types.isUndefinedOrNull(val)) {
|
||||
return true;
|
||||
} else if (types.isString(val)) {
|
||||
return val === compare;
|
||||
} else if (types.isStringArray(val)) {
|
||||
return val.some(item => item === compare);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add provider to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add provider onto
|
||||
*/
|
||||
protected addProvider(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let provider = this.dashboardService.connectionManagementService.connectionInfo.providerId;
|
||||
return config.map((item) => {
|
||||
if (item.provider === undefined) {
|
||||
item.provider = provider;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the edition to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add edition onto
|
||||
*/
|
||||
protected addEdition(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let connectionInfo: ConnectionManagementInfo = this.dashboardService.connectionManagementService.connectionInfo;
|
||||
let edition = connectionInfo.serverInfo.engineEditionId;
|
||||
return config.map((item) => {
|
||||
if (item.edition === undefined) {
|
||||
item.edition = edition;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the context to the passed widgets and returns the new widgets
|
||||
* @param widgets Array of widgets to add context to
|
||||
*/
|
||||
protected addContext(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let context = this.context;
|
||||
return config.map((item) => {
|
||||
if (item.context === undefined) {
|
||||
item.context = context;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates configs to make sure nothing will error out and returns the modified widgets
|
||||
* @param config Array of widgets to validate
|
||||
*/
|
||||
protected removeEmpty(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
return config.filter(widget => {
|
||||
return !types.isUndefinedOrNull(widget);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates configs to make sure nothing will error out and returns the modified widgets
|
||||
* @param config Array of widgets to validate
|
||||
*/
|
||||
protected validateGridConfig(config: WidgetConfig[]): Array<WidgetConfig> {
|
||||
return config.map((widget) => {
|
||||
if (widget.gridItemConfig === undefined) {
|
||||
widget.gridItemConfig = {};
|
||||
}
|
||||
return widget;
|
||||
});
|
||||
}
|
||||
|
||||
protected initExtensionConfigs(configurations: WidgetConfig[]): Array<WidgetConfig> {
|
||||
let widgetRegistry = <IInsightRegistry>Registry.as(Extensions.InsightContribution);
|
||||
return configurations.map((config) => {
|
||||
if (config.widget && Object.keys(config.widget).length === 1) {
|
||||
let key = Object.keys(config.widget)[0];
|
||||
let insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
|
||||
if (insightConfig !== undefined) {
|
||||
// Setup the default properties for this extension if needed
|
||||
if (!config.provider && insightConfig.provider) {
|
||||
config.provider = insightConfig.provider;
|
||||
}
|
||||
if (!config.name && insightConfig.name) {
|
||||
config.name = insightConfig.name;
|
||||
}
|
||||
if (!config.edition && insightConfig.edition) {
|
||||
config.edition = insightConfig.edition;
|
||||
}
|
||||
if (!config.gridItemConfig && insightConfig.gridItemConfig) {
|
||||
config.gridItemConfig = {
|
||||
sizex: insightConfig.gridItemConfig.x,
|
||||
sizey: insightConfig.gridItemConfig.y
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
private getProperties(): Array<WidgetConfig> {
|
||||
let properties = this.dashboardService.getSettings(this.context).properties;
|
||||
if (types.isUndefinedOrNull(properties)) {
|
||||
return [this.propertiesWidget];
|
||||
} else if (types.isBoolean(properties)) {
|
||||
return properties ? [this.propertiesWidget] : [];
|
||||
} else if (types.isArray(properties)) {
|
||||
return properties.map((item) => {
|
||||
let retVal = Object.assign({}, this.propertiesWidget);
|
||||
retVal.edition = item.edition;
|
||||
retVal.provider = item.provider;
|
||||
retVal.widget = { 'properties-widget': { properties: item.properties } };
|
||||
return retVal;
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (this._properties) {
|
||||
this._properties.refresh();
|
||||
}
|
||||
} else {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/sql/parts/dashboard/common/dashboardWidget.ts
Normal file
38
src/sql/parts/dashboard/common/dashboardWidget.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { NgGridItemConfig } from 'angular2-grid';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
|
||||
export interface IDashboardWidget {
|
||||
actions: Array<Action>;
|
||||
actionsContext?: any;
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
export const WIDGET_CONFIG = new InjectionToken<WidgetConfig>('widget_config');
|
||||
|
||||
export interface WidgetConfig {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
context: string;
|
||||
provider: string | Array<string>;
|
||||
edition: number | Array<number>;
|
||||
gridItemConfig?: NgGridItemConfig;
|
||||
widget: Object;
|
||||
background_color?: string;
|
||||
border?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: string;
|
||||
padding?:string;
|
||||
}
|
||||
|
||||
export abstract class DashboardWidget {
|
||||
protected _config: WidgetConfig;
|
||||
|
||||
get actions(): Array<Action> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template component-host>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -0,0 +1,215 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
|
||||
import {
|
||||
Component, Input, Inject, forwardRef, ComponentFactoryResolver, AfterContentInit, ViewChild,
|
||||
ElementRef, OnInit, ChangeDetectorRef, OnDestroy, ReflectiveInjector, Injector, Type, ComponentRef
|
||||
} from '@angular/core';
|
||||
|
||||
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';
|
||||
|
||||
/* Widgets */
|
||||
import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
|
||||
import { ExplorerWidget } from 'sql/parts/dashboard/widgets/explorer/explorerWidget.component';
|
||||
import { TasksWidget } from 'sql/parts/dashboard/widgets/tasks/tasksWidget.component';
|
||||
import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component';
|
||||
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
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';
|
||||
|
||||
const componentMap: { [x: string]: Type<IDashboardWidget> } = {
|
||||
'properties-widget': PropertiesWidgetComponent,
|
||||
'explorer-widget': ExplorerWidget,
|
||||
'tasks-widget': TasksWidget,
|
||||
'insights-widget': InsightsWidget
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-widget-wrapper',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/dashboard/common/dashboardWidgetWrapper.component.html'))
|
||||
})
|
||||
export class DashboardWidgetWrapper implements AfterContentInit, OnInit, OnDestroy {
|
||||
@Input() private _config: WidgetConfig;
|
||||
private _themeDispose: IDisposable;
|
||||
private _actions: Array<Action>;
|
||||
private _component: IDashboardWidget;
|
||||
|
||||
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
|
||||
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => ElementRef)) private _ref: ElementRef,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => Injector)) private _injector: Injector
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
let self = this;
|
||||
self._themeDispose = self._bootstrap.themeService.onDidColorThemeChange((event: IColorTheme) => {
|
||||
self.updateTheme(event);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.updateTheme(this._bootstrap.themeService.getColorTheme());
|
||||
this.loadWidget();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._themeDispose.dispose();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (this._component && this._component.refresh) {
|
||||
this._component.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private loadWidget(): void {
|
||||
if (Object.keys(this._config.widget).length !== 1) {
|
||||
error('Exactly 1 widget must be defined per space');
|
||||
return;
|
||||
}
|
||||
let key = Object.keys(this._config.widget)[0];
|
||||
let selector = this.getOrCreateSelector(key);
|
||||
if (selector === undefined) {
|
||||
error('Could not find selector', key);
|
||||
return;
|
||||
}
|
||||
|
||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(selector);
|
||||
|
||||
let viewContainerRef = this.componentHost.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
let injector = ReflectiveInjector.resolveAndCreate([{ provide: WIDGET_CONFIG, useValue: this._config }], this._injector);
|
||||
let componentRef: ComponentRef<IDashboardWidget>;
|
||||
try {
|
||||
componentRef = viewContainerRef.createComponent(componentFactory, 0, injector);
|
||||
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));
|
||||
}
|
||||
if (actions !== undefined && actions.length > 0) {
|
||||
this._actions = actions;
|
||||
this._changeref.detectChanges();
|
||||
}
|
||||
} catch (e) {
|
||||
error('Error rendering widget', key, e);
|
||||
return;
|
||||
}
|
||||
let el = <HTMLElement>componentRef.location.nativeElement;
|
||||
|
||||
// set widget styles to conform to its box
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.flex = '1 1 auto';
|
||||
el.style.position = 'relative';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the selector for a given key, and if none is defined tries
|
||||
* to load it from the widget registry and configure as needed
|
||||
*
|
||||
* @private
|
||||
* @param {string} key
|
||||
* @returns {Type<IDashboardWidget>}
|
||||
* @memberof DashboardWidgetWrapper
|
||||
*/
|
||||
private getOrCreateSelector(key: string): Type<IDashboardWidget> {
|
||||
let selector = componentMap[key];
|
||||
if (selector === undefined) {
|
||||
// Load the widget from the registry
|
||||
let widgetRegistry = <IInsightRegistry>Registry.as(Extensions.InsightContribution);
|
||||
let insightConfig = widgetRegistry.getRegisteredExtensionInsights(key);
|
||||
if (insightConfig === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// Save the widget for future use
|
||||
selector = componentMap['insights-widget'];
|
||||
this._config.widget['insights-widget'] = insightConfig;
|
||||
}
|
||||
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) {
|
||||
backgroundColor = theme.getColor(this._config.background_color);
|
||||
}
|
||||
|
||||
if (this._config.border === 'none') {
|
||||
borderColor = undefined;
|
||||
}
|
||||
|
||||
if (backgroundColor) {
|
||||
el.style.backgroundColor = backgroundColor.toString();
|
||||
}
|
||||
|
||||
if (foregroundColor) {
|
||||
el.style.color = foregroundColor.toString();
|
||||
}
|
||||
|
||||
let borderString = undefined;
|
||||
if (border) {
|
||||
borderString = border.toString();
|
||||
el.style.borderColor = borderString;
|
||||
el.style.borderWidth = '1px';
|
||||
el.style.borderStyle = 'solid';
|
||||
} else if (borderColor) {
|
||||
borderString = borderColor.toString();
|
||||
el.style.border = '3px solid ' + borderColor.toString();
|
||||
} else {
|
||||
el.style.border = 'none';
|
||||
}
|
||||
|
||||
if (borderString) {
|
||||
headerEl.style.backgroundColor = borderString;
|
||||
} else {
|
||||
headerEl.style.backgroundColor = '';
|
||||
}
|
||||
|
||||
if (this._config.fontSize) {
|
||||
headerEl.style.fontSize = this._config.fontSize;
|
||||
}
|
||||
if (this._config.fontWeight) {
|
||||
headerEl.style.fontWeight = this._config.fontWeight;
|
||||
}
|
||||
if (this._config.padding) {
|
||||
headerEl.style.padding = this._config.padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/sql/parts/dashboard/common/interfaces.ts
Normal file
14
src/sql/parts/dashboard/common/interfaces.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export enum Conditional {
|
||||
'equals',
|
||||
'notEquals',
|
||||
'greaterThanOrEquals',
|
||||
'greaterThan',
|
||||
'lessThanOrEquals',
|
||||
'lessThan',
|
||||
'always'
|
||||
};
|
||||
12
src/sql/parts/dashboard/dashboard.component.html
Normal file
12
src/sql/parts/dashboard/dashboard.component.html
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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: scroll; width: 100%; height: 100%">
|
||||
<div #header style="margin-bottom: 5px">
|
||||
<breadcrumb></breadcrumb>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
58
src/sql/parts/dashboard/dashboard.component.ts
Normal file
58
src/sql/parts/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
|
||||
export const DASHBOARD_SELECTOR: string = 'dashboard-component';
|
||||
|
||||
@Component({
|
||||
selector: DASHBOARD_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./dashboard.component.html'))
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
private _subs: Array<IDisposable> = new Array();
|
||||
@ViewChild('header', { read: ElementRef }) private header: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrapService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
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();
|
||||
if (profile && (!profile.databaseName || Utils.isMaster(profile))) {
|
||||
// Route to the server page as this is the default database
|
||||
self._router.navigate(['server-dashboard']);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._subs.forEach((value) => {
|
||||
value.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
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';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
106
src/sql/parts/dashboard/dashboard.module.ts
Normal file
106
src/sql/parts/dashboard/dashboard.module.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Inject, NgModule, forwardRef, ApplicationRef, ComponentFactoryResolver } from '@angular/core';
|
||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule, Routes, UrlSerializer } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgGridModule } from 'angular2-grid';
|
||||
import { ChartsModule } from 'ng2-charts/ng2-charts';
|
||||
|
||||
import CustomUrlSerializer from 'sql/common/urlSerializer';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
/* Services */
|
||||
import { BreadcrumbService } from 'sql/parts/dashboard/services/breadcrumb.service';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
|
||||
/* Directives */
|
||||
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
|
||||
|
||||
/* Base Components */
|
||||
import { DashboardComponent, DASHBOARD_SELECTOR } from 'sql/parts/dashboard/dashboard.component';
|
||||
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/common/dashboardWidgetWrapper.component';
|
||||
import { BreadcrumbComponent } from 'sql/base/browser/ui/breadcrumb/breadcrumb.component';
|
||||
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
let baseComponents = [DashboardComponent, DashboardWidgetWrapper, ComponentHostDirective, BreadcrumbComponent];
|
||||
|
||||
/* Pages */
|
||||
import { ServerDashboardPage } from 'sql/parts/dashboard/pages/serverDashboardPage.component';
|
||||
import { DatabaseDashboardPage } from 'sql/parts/dashboard/pages/databaseDashboardPage.component';
|
||||
let pageComponents = [ServerDashboardPage, DatabaseDashboardPage];
|
||||
|
||||
/* Widget Components */
|
||||
import { PropertiesWidgetComponent } from 'sql/parts/dashboard/widgets/properties/propertiesWidget.component';
|
||||
import { ExplorerWidget } from 'sql/parts/dashboard/widgets/explorer/explorerWidget.component';
|
||||
import { TasksWidget } from 'sql/parts/dashboard/widgets/tasks/tasksWidget.component';
|
||||
import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component';
|
||||
let widgetComponents = [PropertiesWidgetComponent, ExplorerWidget, TasksWidget, InsightsWidget];
|
||||
|
||||
/* Insights */
|
||||
let insightComponents = Registry.as<IInsightRegistry>(Extensions.InsightContribution).getAllCtors();
|
||||
|
||||
// Setup routes for various child components
|
||||
const appRoutes: Routes = [
|
||||
{ path: 'database-dashboard', component: DatabaseDashboardPage },
|
||||
{ path: 'server-dashboard', component: ServerDashboardPage },
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'database-dashboard',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{ path: '**', component: DatabaseDashboardPage }
|
||||
];
|
||||
|
||||
// Connection Dashboard main angular module
|
||||
@NgModule({
|
||||
declarations: [
|
||||
...baseComponents,
|
||||
...pageComponents,
|
||||
...widgetComponents,
|
||||
...insightComponents
|
||||
],
|
||||
// also for widgets
|
||||
entryComponents: [
|
||||
DashboardComponent,
|
||||
...widgetComponents,
|
||||
...insightComponents
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
NgGridModule,
|
||||
ChartsModule,
|
||||
RouterModule.forRoot(appRoutes)
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
{ provide: IBreadcrumbService, useClass: BreadcrumbService },
|
||||
DashboardServiceInterface,
|
||||
{ provide: UrlSerializer, useClass: CustomUrlSerializer }
|
||||
]
|
||||
})
|
||||
export class DashboardModule {
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface
|
||||
) {
|
||||
}
|
||||
|
||||
ngDoBootstrap(appRef: ApplicationRef) {
|
||||
const factory = this._resolver.resolveComponentFactory(DashboardComponent);
|
||||
const uniqueSelector: string = this._bootstrapService.getUniqueSelector(DASHBOARD_SELECTOR);
|
||||
this._bootstrap.selector = uniqueSelector;
|
||||
(<any>factory).factory.selector = uniqueSelector;
|
||||
appRef.bootstrap(factory);
|
||||
}
|
||||
}
|
||||
22
src/sql/parts/dashboard/dashboardConfig.contribution.ts
Normal file
22
src/sql/parts/dashboard/dashboardConfig.contribution.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { DATABASE_DASHBOARD_SETTING, DATABASE_DASHBOARD_PROPERTIES, databaseDashboardSettingSchema, databaseDashboardPropertiesSchema } from 'sql/parts/dashboard/pages/databaseDashboardPage.contribution';
|
||||
import { SERVER_DASHBOARD_SETTING, SERVER_DASHBOARD_PROPERTIES, serverDashboardSettingSchema, serverDashboardPropertiesSchema } from 'sql/parts/dashboard/pages/serverDashboardPage.contribution';
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
const dashboardConfig: IConfigurationNode = {
|
||||
id: 'Dashboard',
|
||||
type: 'object',
|
||||
properties: {
|
||||
[DATABASE_DASHBOARD_PROPERTIES]: databaseDashboardPropertiesSchema,
|
||||
[SERVER_DASHBOARD_PROPERTIES]: serverDashboardPropertiesSchema,
|
||||
[DATABASE_DASHBOARD_SETTING]: databaseDashboardSettingSchema,
|
||||
[SERVER_DASHBOARD_SETTING]: serverDashboardSettingSchema
|
||||
}
|
||||
};
|
||||
|
||||
configurationRegistry.registerConfiguration(dashboardConfig);
|
||||
112
src/sql/parts/dashboard/dashboardEditor.ts
Normal file
112
src/sql/parts/dashboard/dashboardEditor.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Dimension, Builder, $ } from 'vs/base/browser/builder';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
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';
|
||||
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { DashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { DASHBOARD_SELECTOR } from 'sql/parts/dashboard/dashboard.component';
|
||||
|
||||
export class DashboardEditor extends BaseEditor {
|
||||
|
||||
public static ID: string = 'workbench.editor.connectiondashboard';
|
||||
private _dashboardContainer: HTMLElement;
|
||||
protected _input: DashboardInput;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IBootstrapService private _bootstrapService: IBootstrapService
|
||||
) {
|
||||
super(DashboardEditor.ID, telemetryService, themeService);
|
||||
}
|
||||
|
||||
public get input(): DashboardInput {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to create the editor in the parent builder.
|
||||
*/
|
||||
public createEditor(parent: Builder): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
|
||||
*/
|
||||
public focus(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
|
||||
* To be called when the container of this editor changes size.
|
||||
*/
|
||||
public layout(dimension: Dimension): void {
|
||||
}
|
||||
|
||||
public setInput(input: DashboardInput, options: EditorOptions): TPromise<void> {
|
||||
if (this.input && this.input.matches(input)) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
const parentElement = this.getContainer().getHTMLElement();
|
||||
|
||||
super.setInput(input, options);
|
||||
|
||||
$(parentElement).empty();
|
||||
|
||||
if (!input.hasBootstrapped) {
|
||||
let container = DOM.$<HTMLElement>('.dashboardEditor');
|
||||
container.style.height = '100%';
|
||||
this._dashboardContainer = DOM.append(parentElement, container);
|
||||
this.input.container = this._dashboardContainer;
|
||||
return TPromise.wrap(input.initializedPromise.then(() => this.bootstrapAngular(input)));
|
||||
} else {
|
||||
this._dashboardContainer = DOM.append(parentElement, this.input.container);
|
||||
return TPromise.as<void>(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the angular components and record for this input that we have done so
|
||||
*/
|
||||
private bootstrapAngular(input: DashboardInput): void {
|
||||
// Get the bootstrap params and perform the bootstrap
|
||||
let params: DashboardComponentParams = {
|
||||
connection: input.connectionProfile,
|
||||
ownerUri: input.uri
|
||||
};
|
||||
|
||||
input.hasBootstrapped = true;
|
||||
|
||||
let uniqueSelector = this._bootstrapService.bootstrap(
|
||||
DashboardModule,
|
||||
this._dashboardContainer,
|
||||
DASHBOARD_SELECTOR,
|
||||
params,
|
||||
input);
|
||||
input.setUniqueSelector(uniqueSelector);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
ModesRegistry.registerLanguage({
|
||||
extensions: ['.dashboard'],
|
||||
id: 'dashboard',
|
||||
});
|
||||
170
src/sql/parts/dashboard/dashboardInput.ts
Normal file
170
src/sql/parts/dashboard/dashboardInput.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { EditorInput, EditorModel } from 'vs/workbench/common/editor';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
|
||||
export class DashboardInput extends EditorInput {
|
||||
|
||||
private _uri: string;
|
||||
public static ID: string = 'workbench.editorinputs.connectiondashboardinputs';
|
||||
public static SCHEMA: string = 'sqldashboard';
|
||||
|
||||
private _initializedPromise: Thenable<void>;
|
||||
private _onConnectionChanged: IDisposable;
|
||||
|
||||
public get initializedPromise(): Thenable<void> {
|
||||
return this._initializedPromise;
|
||||
}
|
||||
|
||||
private _uniqueSelector: string;
|
||||
|
||||
public hasBootstrapped = false;
|
||||
// Holds the HTML content for the editor when the editor discards this input and loads another
|
||||
private _parentContainer: HTMLElement;
|
||||
|
||||
constructor(
|
||||
_connectionProfile: IConnectionProfile,
|
||||
@IConnectionManagementService private _connectionService: IConnectionManagementService,
|
||||
@IModeService modeService: IModeService,
|
||||
@IModelService model: IModelService
|
||||
) {
|
||||
super();
|
||||
// TODO; possible refactor
|
||||
// basically this is mimicing creating a "model" (the backing model for text for editors)
|
||||
// for dashboard, even though there is no backing text. We need this so that we can
|
||||
// tell the icon theme services that we are a dashboard resource, therefore loading the correct icon
|
||||
|
||||
// vscode has a comment that Mode's will eventually be removed (not sure the state of this comment)
|
||||
// so this might be able to be undone when that happens
|
||||
if (!model.getModel(this.getResource())) {
|
||||
model.createModel('', modeService.getMode('dashboard'), this.getResource());
|
||||
}
|
||||
this._initializedPromise = _connectionService.connectIfNotConnected(_connectionProfile, 'dashboard').then(
|
||||
u => {
|
||||
this._uri = u;
|
||||
let info = this._connectionService.getConnectionInfo(u);
|
||||
if (info) {
|
||||
this._onConnectionChanged = this._connectionService.onConnectionChanged(e => {
|
||||
if (e.connectionUri === u) {
|
||||
this._onDidChangeLabel.fire();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public setUniqueSelector(uniqueSelector: string): void {
|
||||
this._uniqueSelector = uniqueSelector;
|
||||
}
|
||||
|
||||
public getTypeId(): string {
|
||||
return UntitledEditorInput.ID;
|
||||
}
|
||||
|
||||
public getResource(): URI {
|
||||
return URI.from({
|
||||
scheme: 'dashboard',
|
||||
path: '.dashboard'
|
||||
});
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
if (!this.connectionProfile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let name = this.connectionProfile.serverName;
|
||||
if (this.connectionProfile.databaseName
|
||||
&& !this.isMasterMssql()) {
|
||||
// Only add DB name if this is a non-default, non-master connection
|
||||
name = name + ':' + this.connectionProfile.databaseName;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private isMasterMssql(): boolean {
|
||||
return this.connectionProfile.providerName.toLowerCase() === 'mssql'
|
||||
&& this.connectionProfile.databaseName.toLowerCase() === 'master';
|
||||
}
|
||||
|
||||
public get uri(): string {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposeContainer();
|
||||
if (this._onConnectionChanged) {
|
||||
this._onConnectionChanged.dispose();
|
||||
}
|
||||
this._connectionService.disconnect(this._uri);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _disposeContainer() {
|
||||
if (!this._parentContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentNode = this._parentContainer.parentNode;
|
||||
if (parentNode) {
|
||||
parentNode.removeChild(this._parentContainer);
|
||||
this._parentContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
set container(container: HTMLElement) {
|
||||
this._disposeContainer();
|
||||
this._parentContainer = container;
|
||||
}
|
||||
|
||||
get container(): HTMLElement {
|
||||
return this._parentContainer;
|
||||
}
|
||||
|
||||
public supportsSplitEditor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public get connectionProfile(): IConnectionProfile {
|
||||
return this._connectionService.getConnectionProfile(this._uri);
|
||||
}
|
||||
|
||||
public resolve(refresh?: boolean): TPromise<EditorModel> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get hasInitialized(): boolean {
|
||||
return !!this._uniqueSelector;
|
||||
}
|
||||
|
||||
public get uniqueSelector(): string {
|
||||
return this._uniqueSelector;
|
||||
}
|
||||
|
||||
public matches(otherinput: any): boolean {
|
||||
return otherinput instanceof DashboardInput
|
||||
&& DashboardInput.profileMatches(this.connectionProfile, otherinput.connectionProfile);
|
||||
}
|
||||
|
||||
// similar to the default profile match but without databasename
|
||||
public static profileMatches(profile1: IConnectionProfile, profile2: IConnectionProfile): boolean {
|
||||
return profile1 && profile2
|
||||
&& profile1.providerName === profile2.providerName
|
||||
&& profile1.serverName === profile2.serverName
|
||||
&& profile1.userName === profile2.userName
|
||||
&& profile1.authenticationType === profile2.authenticationType
|
||||
&& profile1.groupFullName === profile2.groupFullName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OnInit, Inject, forwardRef, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
|
||||
import { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
|
||||
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
|
||||
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
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 {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('databasePageName', 'DATABASE DASHBOARD'),
|
||||
widget: {
|
||||
'properties-widget': undefined
|
||||
},
|
||||
context: 'database',
|
||||
background_color: colors.editorBackground,
|
||||
border: 'none',
|
||||
fontSize: '14px',
|
||||
fontWeight: '200',
|
||||
padding: '5px 0 0 0',
|
||||
provider: undefined,
|
||||
edition: undefined
|
||||
};
|
||||
|
||||
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
|
||||
) {
|
||||
super(dashboardService);
|
||||
this._dispose.push(dashboardService.onUpdatePage(() => {
|
||||
this.refresh(true);
|
||||
this._cd.detectChanges();
|
||||
}));
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._breadcrumbService.setBreadcrumbs(BreadcrumbClass.DatabasePage);
|
||||
this.baseInit();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._dispose = dispose(this._dispose);
|
||||
this.baseDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { Extensions, IDashboardWidgetRegistry } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
let widgetRegistry = <IDashboardWidgetRegistry>Registry.as(Extensions.DashboardWidgetContribution);
|
||||
|
||||
export const databaseDashboardPropertiesSchema: IJSONSchema = {
|
||||
description: nls.localize('dashboardDatabaseProperties', 'Enable or disable the properties widget'),
|
||||
default: true,
|
||||
oneOf: <IJSONSchema[]>[
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string'
|
||||
},
|
||||
edition: {
|
||||
type: 'number'
|
||||
},
|
||||
properties: {
|
||||
description: nls.localize('dashboard.databaseproperties', 'Property values to show'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.databaseproperties.displayName', 'Display name of the property')
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.databaseproperties.value', 'Value in the Database Info Object')
|
||||
},
|
||||
ignore: {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.databaseproperties.ignore', 'Specify specific values to ignore'),
|
||||
items: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
displayName: nls.localize('recoveryModel', 'Recovery Model'),
|
||||
value: 'recoveryModel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastDatabaseBackup', 'Last Database Backup'),
|
||||
value: 'lastBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('lastLogBackup', 'Last Log Backup'),
|
||||
value: 'lastLogBackupDate',
|
||||
ignore: [
|
||||
'1/1/0001 12:00:00 AM'
|
||||
]
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('compatibilityLevel', 'Compatibility Level'),
|
||||
value: 'compatibilityLevel'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('owner', 'Owner'),
|
||||
value: 'owner'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const databaseDashboardSettingSchema: IJSONSchema = {
|
||||
type: ['array'],
|
||||
description: nls.localize('dashboardDatabase', 'Customizes the database dashboard page'),
|
||||
items: <IJSONSchema>{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
},
|
||||
provider: {
|
||||
anyOf: [
|
||||
'string',
|
||||
{
|
||||
type: 'array',
|
||||
items: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
edition: {
|
||||
anyOf: [
|
||||
'number',
|
||||
{
|
||||
type: 'array',
|
||||
items: 'number'
|
||||
}
|
||||
]
|
||||
},
|
||||
gridItemConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sizex: {
|
||||
type: 'number'
|
||||
},
|
||||
sizey: {
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
},
|
||||
widget: {
|
||||
type: 'object',
|
||||
properties: widgetRegistry.databaseWidgetSchema.properties,
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
name: 'Tasks',
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 1
|
||||
},
|
||||
widget: {
|
||||
'tasks-widget': {}
|
||||
}
|
||||
},
|
||||
{
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 2
|
||||
},
|
||||
widget: {
|
||||
'explorer-widget': {}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const DATABASE_DASHBOARD_SETTING = 'dashboard.database.widgets';
|
||||
export const DATABASE_DASHBOARD_PROPERTIES = 'dashboard.database.properties';
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { DashboardPage } from 'sql/parts/dashboard/common/dashboardPage.component';
|
||||
import { BreadcrumbClass } from 'sql/parts/dashboard/services/breadcrumb.service';
|
||||
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export class ServerDashboardPage extends DashboardPage implements OnInit, OnDestroy {
|
||||
protected propertiesWidget: WidgetConfig = {
|
||||
name: nls.localize('serverPageName', 'SERVER DASHBOARD'),
|
||||
widget: {
|
||||
'properties-widget': undefined
|
||||
},
|
||||
context: 'server',
|
||||
background_color: colors.editorBackground,
|
||||
border: 'none',
|
||||
fontSize: '14px',
|
||||
fontWeight: '200',
|
||||
padding: '5px 0 0 0',
|
||||
provider: undefined,
|
||||
edition: undefined
|
||||
};
|
||||
|
||||
protected readonly context = 'server';
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => IBreadcrumbService)) private breadcrumbService: IBreadcrumbService,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) dashboardService: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef
|
||||
) {
|
||||
super(dashboardService);
|
||||
// revert back to default database
|
||||
this.dashboardService.connectionManagementService.changeDatabase('master').then(() => {
|
||||
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = undefined;
|
||||
this.init();
|
||||
cd.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.breadcrumbService.setBreadcrumbs(BreadcrumbClass.ServerPage);
|
||||
this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.databaseName = null;
|
||||
this.baseInit();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.baseDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { Extensions, IDashboardWidgetRegistry } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
let widgetRegistry = <IDashboardWidgetRegistry>Registry.as(Extensions.DashboardWidgetContribution);
|
||||
|
||||
export interface IPropertiesConfig {
|
||||
edition: number | Array<number>;
|
||||
provider: string | Array<string>;
|
||||
properties: {
|
||||
displayName: string;
|
||||
value: string
|
||||
}[];
|
||||
}
|
||||
|
||||
export const serverDashboardPropertiesSchema: IJSONSchema = {
|
||||
description: nls.localize('dashboardServerProperties', 'Enable or disable the properties widget'),
|
||||
default: true,
|
||||
oneOf: [
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string'
|
||||
},
|
||||
edition: {
|
||||
type: 'number'
|
||||
},
|
||||
properties: {
|
||||
description: nls.localize('dashboard.serverproperties', 'Property values to show'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.serverproperties.displayName', 'Display name of the property')
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.serverproperties.value', 'Value in the Server Info Object')
|
||||
}
|
||||
}
|
||||
},
|
||||
default: [
|
||||
{
|
||||
displayName: nls.localize('version', 'Version'),
|
||||
value: 'serverVersion'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('edition', 'Edition'),
|
||||
value: 'serverEdition'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('computerName', 'Computer Name'),
|
||||
value: 'machineName'
|
||||
},
|
||||
{
|
||||
displayName: nls.localize('osVersion', 'OS Version'),
|
||||
value: 'osVersion'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let defaultVal = [
|
||||
{
|
||||
name: 'Tasks',
|
||||
widget: {
|
||||
'tasks-widget': {}
|
||||
},
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
gridItemConfig: {
|
||||
sizex: 1,
|
||||
sizey: 2
|
||||
},
|
||||
widget: {
|
||||
'explorer-widget': {}
|
||||
}
|
||||
},
|
||||
{
|
||||
widget: {
|
||||
'backup-history-server-insight': {
|
||||
cacheId: '0c7cba8b-c87a-4bcc-ae54-2f40a5503a90'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
widget: {
|
||||
'all-database-size-server-insight': {
|
||||
cacheId: '1d7cba8b-c87a-4bcc-ae54-2f40a5503a90'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const serverDashboardSettingSchema: IJSONSchema = {
|
||||
type: ['array'],
|
||||
description: nls.localize('dashboardServer', 'Customizes the server dashboard page'),
|
||||
items: <IJSONSchema>{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
},
|
||||
provider: {
|
||||
anyOf: [
|
||||
'string',
|
||||
{
|
||||
type: 'array',
|
||||
items: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
edition: {
|
||||
anyOf: [
|
||||
'number',
|
||||
{
|
||||
type: 'array',
|
||||
items: 'number'
|
||||
}
|
||||
]
|
||||
},
|
||||
gridItemConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sizex: {
|
||||
type: 'number'
|
||||
},
|
||||
sizey: {
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
},
|
||||
widget: {
|
||||
type: 'object',
|
||||
properties: widgetRegistry.serverWidgetSchema.properties,
|
||||
maxItems: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
default: defaultVal
|
||||
};
|
||||
|
||||
export const SERVER_DASHBOARD_SETTING = 'dashboard.server.widgets';
|
||||
export const SERVER_DASHBOARD_PROPERTIES = 'dashboard.server.properties';
|
||||
74
src/sql/parts/dashboard/services/breadcrumb.service.ts
Normal file
74
src/sql/parts/dashboard/services/breadcrumb.service.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Injectable, forwardRef, Inject, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { DashboardServiceInterface } from './dashboardServiceInterface.service';
|
||||
import { MenuItem, IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
|
||||
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
|
||||
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export enum BreadcrumbClass {
|
||||
DatabasePage,
|
||||
ServerPage
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class BreadcrumbService implements IBreadcrumbService, OnDestroy {
|
||||
public breadcrumbItem: Subject<MenuItem[]>;
|
||||
private itemBreadcrums: MenuItem[];
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _currentPage: BreadcrumbClass;
|
||||
|
||||
constructor( @Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface) {
|
||||
_bootstrap.onUpdatePage(() => {
|
||||
this.setBreadcrumbs(this._currentPage);
|
||||
});
|
||||
this.breadcrumbItem = new Subject<MenuItem[]>();
|
||||
}
|
||||
|
||||
public setBreadcrumbs(page: BreadcrumbClass) {
|
||||
this._currentPage = page;
|
||||
this.itemBreadcrums = [];
|
||||
let refList: MenuItem[] = this.getBreadcrumbsLink(page);
|
||||
this.breadcrumbItem.next(refList);
|
||||
}
|
||||
|
||||
private getBreadcrumbsLink(page: BreadcrumbClass): MenuItem[] {
|
||||
this.itemBreadcrums = [];
|
||||
let profile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile;
|
||||
this.itemBreadcrums.push({ label: nls.localize('homeCrumb', 'Home')});
|
||||
switch (page) {
|
||||
case BreadcrumbClass.DatabasePage:
|
||||
this.itemBreadcrums.push(this.getServerBreadcrumb(profile));
|
||||
this.itemBreadcrums.push(this.getDbBreadcrumb(profile));
|
||||
break;
|
||||
case BreadcrumbClass.ServerPage:
|
||||
this.itemBreadcrums.push(this.getServerBreadcrumb(profile));
|
||||
break;
|
||||
default:
|
||||
this.itemBreadcrums = [];
|
||||
}
|
||||
return this.itemBreadcrums;
|
||||
}
|
||||
|
||||
private getServerBreadcrumb(profile: ConnectionProfile): MenuItem {
|
||||
return { label: profile.serverName, routerLink: ['server-dashboard'] };
|
||||
}
|
||||
|
||||
private getDbBreadcrumb(profile: ConnectionProfile): MenuItem {
|
||||
return {
|
||||
label: profile.databaseName ? profile.databaseName : 'database-name',
|
||||
routerLink: ['database-dashboard']
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Node Modules */
|
||||
import { Injectable, Inject, forwardRef, OnDestroy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
/* SQL imports */
|
||||
import { DashboardComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { IMetadataService } from 'sql/services/metadata/metadataService';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo';
|
||||
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 { ProviderMetadata, DatabaseInfo, SimpleExecuteResult } from 'data';
|
||||
|
||||
/* VS imports */
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
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 { IMessageService } from 'vs/platform/message/common/message';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
const DASHBOARD_SETTINGS = 'dashboard';
|
||||
|
||||
/* Wrapper for a metadata service that contains the uri string to use on each request */
|
||||
export class SingleConnectionMetadataService {
|
||||
|
||||
constructor(
|
||||
private _metadataService: IMetadataService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
get metadata(): Observable<ProviderMetadata> {
|
||||
return Observable.fromPromise(this._metadataService.getMetadata(this._uri));
|
||||
}
|
||||
|
||||
get databaseNames(): Observable<string[]> {
|
||||
return Observable.fromPromise(this._metadataService.getDatabaseNames(this._uri));
|
||||
}
|
||||
}
|
||||
|
||||
/* Wrapper for a connection service that contains the uri string to use on each request */
|
||||
export class SingleConnectionManagementService {
|
||||
|
||||
constructor(
|
||||
private _connectionService: IConnectionManagementService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
public changeDatabase(name: string): Thenable<boolean> {
|
||||
return this._connectionService.changeDatabase(this._uri, name);
|
||||
}
|
||||
|
||||
public get connectionInfo(): ConnectionManagementInfo {
|
||||
return this._connectionService.getConnectionInfo(this._uri);
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleAdminService {
|
||||
|
||||
constructor(
|
||||
private _adminService: IAdminService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
public get databaseInfo(): Observable<DatabaseInfo> {
|
||||
return Observable.fromPromise(this._adminService.getDatabaseInfo(this._uri));
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleQueryManagementService {
|
||||
constructor(
|
||||
private _queryManagementService: IQueryManagementService,
|
||||
private _uri: string
|
||||
) { }
|
||||
|
||||
public runQueryAndReturn(queryString: string): Thenable<SimpleExecuteResult> {
|
||||
return this._queryManagementService.runQueryAndReturn(this._uri, queryString);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Providers a interface between a dashboard interface and the rest of carbon.
|
||||
Stores the uri and unique selector of a dashboard instance and uses that
|
||||
whenever a call to a carbon service needs this information, so that the widgets
|
||||
don't need to be aware of the uri or selector. Simplifies the initialization and
|
||||
usage of a widget.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DashboardServiceInterface implements OnDestroy {
|
||||
private _uniqueSelector: string;
|
||||
private _uri: string;
|
||||
private _bootstrapParams: DashboardComponentParams;
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
/* Services */
|
||||
private _metadataService: SingleConnectionMetadataService;
|
||||
private _connectionManagementService: SingleConnectionManagementService;
|
||||
private _themeService: IWorkbenchThemeService;
|
||||
private _contextMenuService: IContextMenuService;
|
||||
private _instantiationService: IInstantiationService;
|
||||
private _adminService: SingleAdminService;
|
||||
private _queryManagementService: SingleQueryManagementService;
|
||||
private _configService: IConfigurationService;
|
||||
private _insightsDialogService: IInsightsDialogService;
|
||||
private _contextViewService: IContextViewService;
|
||||
private _messageService: IMessageService;
|
||||
private _workspaceContextService: IWorkspaceContextService;
|
||||
private _storageService: IStorageService;
|
||||
private _capabilitiesService: ICapabilitiesService;
|
||||
|
||||
private _updatePage = new Emitter<void>();
|
||||
public readonly onUpdatePage: Event<void> = this._updatePage.event;
|
||||
|
||||
constructor(
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
) {
|
||||
this._themeService = this._bootstrapService.themeService;
|
||||
this._contextMenuService = this._bootstrapService.contextMenuService;
|
||||
this._instantiationService = this._bootstrapService.instantiationService;
|
||||
this._configService = this._bootstrapService.configurationService;
|
||||
this._insightsDialogService = this._bootstrapService.insightsDialogService;
|
||||
this._contextViewService = this._bootstrapService.contextViewService;
|
||||
this._messageService = this._bootstrapService.messageService;
|
||||
this._workspaceContextService = this._bootstrapService.workspaceContextService;
|
||||
this._storageService = this._bootstrapService.storageService;
|
||||
this._capabilitiesService = this._bootstrapService.capabilitiesService;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach((item) => item.dispose());
|
||||
}
|
||||
|
||||
public get messageService(): IMessageService {
|
||||
return this._messageService;
|
||||
}
|
||||
|
||||
public get metadataService(): SingleConnectionMetadataService {
|
||||
return this._metadataService;
|
||||
}
|
||||
|
||||
public get connectionManagementService(): SingleConnectionManagementService {
|
||||
return this._connectionManagementService;
|
||||
}
|
||||
|
||||
public get themeService(): IWorkbenchThemeService {
|
||||
return this._themeService;
|
||||
}
|
||||
|
||||
public get contextMenuService(): IContextMenuService {
|
||||
return this._contextMenuService;
|
||||
}
|
||||
|
||||
public get instantiationService(): IInstantiationService {
|
||||
return this._instantiationService;
|
||||
}
|
||||
|
||||
public get adminService(): SingleAdminService {
|
||||
return this._adminService;
|
||||
}
|
||||
|
||||
public get queryManagementService(): SingleQueryManagementService {
|
||||
return this._queryManagementService;
|
||||
}
|
||||
|
||||
public get contextViewService(): IContextViewService {
|
||||
return this._contextViewService;
|
||||
}
|
||||
|
||||
public get workspaceContextService(): IWorkspaceContextService {
|
||||
return this._workspaceContextService;
|
||||
}
|
||||
|
||||
public get storageService(): IStorageService {
|
||||
return this._storageService;
|
||||
}
|
||||
|
||||
public get CapabilitiesService(): ICapabilitiesService {
|
||||
return this._capabilitiesService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selector for this dashboard instance, should only be set once
|
||||
*/
|
||||
public set selector(selector: string) {
|
||||
this._uniqueSelector = selector;
|
||||
this._getbootstrapParams();
|
||||
}
|
||||
|
||||
private _getbootstrapParams(): void {
|
||||
this._bootstrapParams = this._bootstrapService.getBootstrapParams(this._uniqueSelector);
|
||||
this.uri = this._bootstrapParams.ownerUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the uri for this dashboard instance, should only be set once
|
||||
* Inits all the services that depend on knowing a uri
|
||||
*/
|
||||
private set uri(uri: string) {
|
||||
this._uri = uri;
|
||||
this._metadataService = new SingleConnectionMetadataService(this._bootstrapService.metadataService, this._uri);
|
||||
this._connectionManagementService = new SingleConnectionManagementService(this._bootstrapService.connectionManagementService, this._uri);
|
||||
this._adminService = new SingleAdminService(this._bootstrapService.adminService, this._uri);
|
||||
this._queryManagementService = new SingleQueryManagementService(this._bootstrapService.queryManagementService, this._uri);
|
||||
this._disposables.push(toDisposableSubscription(this._bootstrapService.angularEventingService.onAngularEvent(this._uri, (event) => this.handleDashboardEvent(event))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying Uri for dashboard
|
||||
* In general don't use this, use specific services instances exposed publically
|
||||
*/
|
||||
public getUnderlyingUri(): string {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public getOriginalConnectionProfile(): IConnectionProfile {
|
||||
return this._bootstrapParams.connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
private handleDashboardEvent(event: AngularEventType): void {
|
||||
switch (event) {
|
||||
case AngularEventType.NAV_DATABASE:
|
||||
this.connectionManagementService.changeDatabase(this.connectionManagementService.connectionInfo.connectionProfile.databaseName).then(
|
||||
result => {
|
||||
if (result) {
|
||||
if (this._router.url === '/database-dashboard') {
|
||||
this._updatePage.fire();
|
||||
} else {
|
||||
this._router.navigate(['database-dashboard']);
|
||||
}
|
||||
} else {
|
||||
this.messageService.show(Severity.Error, nls.localize('dashboard.changeDatabaseFailure', "Failed to change database"));
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.messageService.show(Severity.Error, nls.localize('dashboard.changeDatabaseFailure', "Failed to change database"));
|
||||
}
|
||||
);
|
||||
break;
|
||||
case AngularEventType.NAV_SERVER:
|
||||
this._router.navigate(['server-dashboard']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/sql/parts/dashboard/widgets/explorer/explorerActions.ts
Normal file
89
src/sql/parts/dashboard/widgets/explorer/explorerActions.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div class="explorer-widget" style="display: flex; flex-flow: column; position: absolute; height:100%; width:100%; padding: 10px; box-sizing: border-box">
|
||||
<div #input style="width: 100%"></div>
|
||||
<div style="flex: 1 1 auto; position: relative">
|
||||
<div #table style="position: absolute; height: 100%; width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,394 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!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 { 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 { 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 { 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> = [];
|
||||
private _input: InputBox;
|
||||
private _table: List<ListResource>;
|
||||
private _lastClickedItem: ListResource;
|
||||
private _filterDelayer = new Delayer<void>(200);
|
||||
private _dblClickDelayer = new MultipleRequestDelayer<void>(500);
|
||||
|
||||
@ViewChild('input') private _inputContainer: ElementRef;
|
||||
@ViewChild('table') private _tableContainer: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private _bootstrap: DashboardServiceInterface,
|
||||
@Inject(forwardRef(() => Router)) private _router: Router,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
|
||||
) {
|
||||
super();
|
||||
this._isCloud = _bootstrap.connectionManagementService.connectionInfo.serverInfo.isCloud;
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let inputOptions: IInputOptions = {
|
||||
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._filterDelayer.trigger(() => {
|
||||
this._table.splice(0, this._table.length, this._filterTable(e));
|
||||
});
|
||||
}));
|
||||
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());
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
if (this._config.context === 'database') {
|
||||
this._disposables.push(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);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.objectError', "Unable to load objects");
|
||||
}
|
||||
)));
|
||||
} else {
|
||||
this._disposables.push(toDisposableSubscription(this._bootstrap.metadataService.databaseNames.subscribe(
|
||||
data => {
|
||||
this._tableData = data;
|
||||
this._table.splice(0, this._table.length, this._tableData);
|
||||
},
|
||||
error => {
|
||||
(<HTMLElement>this._el.nativeElement).innerText = nls.localize('dashboard.explorer.databaseError', "Unable to load databases");
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { registerDashboardWidget } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
|
||||
let explorerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
registerDashboardWidget('explorer-widget', '', explorerSchema);
|
||||
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
explorer-widget .list-row .icon {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
explorer-widget .list-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
37
src/sql/parts/dashboard/widgets/insights/actions.ts
Normal file
37
src/sql/parts/dashboard/widgets/insights/actions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { RunQueryOnConnectionMode, IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
|
||||
import { InsightActionContext } from 'sql/workbench/common/actions';
|
||||
|
||||
export class RunInsightQueryAction extends Action {
|
||||
public static ID = 'runQuery';
|
||||
public static LABEL = nls.localize('insights.runQuery', "Run Query");
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IQueryEditorService protected _queryEditorService: IQueryEditorService,
|
||||
@IConnectionManagementService protected _connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(context: InsightActionContext): TPromise<boolean> {
|
||||
return new TPromise<boolean>((resolve, reject) => {
|
||||
TaskUtilities.newQuery(
|
||||
context.profile,
|
||||
this._connectionManagementService,
|
||||
this._queryEditorService,
|
||||
context.insight.query as string,
|
||||
RunQueryOnConnectionMode.executeQuery
|
||||
).then(() => resolve(true), () => resolve(false));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import {
|
||||
Component, Inject, ViewContainerRef, forwardRef, AfterContentInit,
|
||||
ComponentFactoryResolver, ViewChild, OnDestroy, ChangeDetectorRef
|
||||
} from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { DashboardWidget, IDashboardWidget, WIDGET_CONFIG, WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
|
||||
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
|
||||
import { InsightAction, InsightActionContext } from 'sql/workbench/common/actions';
|
||||
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 { 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';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
@Component({
|
||||
selector: 'insights-widget',
|
||||
template: `
|
||||
<div *ngIf="error" style="text-align: center; padding-top: 20px">{{error}}</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%}']
|
||||
})
|
||||
export class InsightsWidget extends DashboardWidget implements IDashboardWidget, AfterContentInit, OnDestroy {
|
||||
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;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => DashboardServiceInterface)) private dashboardService: DashboardServiceInterface,
|
||||
@Inject(WIDGET_CONFIG) protected _config: WidgetConfig,
|
||||
@Inject(forwardRef(() => ViewContainerRef)) private viewContainerRef: ViewContainerRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
this.insightConfig = <IInsightsConfig>this._config.widget['insights-widget'];
|
||||
|
||||
this._verifyConfig();
|
||||
|
||||
this._parseConfig().then(() => {
|
||||
if (!this._checkStorage()) {
|
||||
let promise = this._runQuery();
|
||||
this.queryObv = Observable.fromPromise(promise);
|
||||
promise.then(
|
||||
result => {
|
||||
if (this._init) {
|
||||
this._updateChild(result);
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(result));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if (this._init) {
|
||||
this.showError(error);
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.reject<SimpleExecuteResult>(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}, error => {
|
||||
this.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._init = true;
|
||||
if (this.queryObv) {
|
||||
this._disposables.push(toDisposableSubscription(this.queryObv.subscribe(
|
||||
result => {
|
||||
this._updateChild(result);
|
||||
},
|
||||
error => {
|
||||
this.showError(error);
|
||||
}
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach(i => i.dispose());
|
||||
}
|
||||
|
||||
private showError(error: string): void {
|
||||
this.error = error;
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
get actions(): Array<Action> {
|
||||
let actions: Array<Action> = [];
|
||||
if (this.insightConfig.details && (this.insightConfig.details.query || this.insightConfig.details.queryFile)) {
|
||||
actions.push(this.dashboardService.instantiationService.createInstance(InsightAction, InsightAction.ID, InsightAction.LABEL));
|
||||
}
|
||||
actions.push(this.dashboardService.instantiationService.createInstance(RunInsightQueryAction, RunInsightQueryAction.ID, RunInsightQueryAction.LABEL));
|
||||
return actions;
|
||||
}
|
||||
|
||||
get actionsContext(): InsightActionContext {
|
||||
return <InsightActionContext>{
|
||||
profile: this.dashboardService.connectionManagementService.connectionInfo.connectionProfile,
|
||||
insight: this.insightConfig
|
||||
};
|
||||
}
|
||||
|
||||
private _storeResult(result: SimpleExecuteResult): SimpleExecuteResult {
|
||||
if (this.insightConfig.cacheId) {
|
||||
this.dashboardService.storageService.store(this._getStorageKey(), JSON.stringify(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _checkStorage(): boolean {
|
||||
if (this.insightConfig.cacheId) {
|
||||
let storage = this.dashboardService.storageService.get(this._getStorageKey());
|
||||
if (storage) {
|
||||
if (this._init) {
|
||||
this._updateChild(JSON.parse(storage));
|
||||
} else {
|
||||
this.queryObv = Observable.fromPromise(Promise.resolve<SimpleExecuteResult>(JSON.parse(storage)));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public get refresh(): () => void {
|
||||
return this._refresh();
|
||||
}
|
||||
|
||||
public _refresh(): () => void {
|
||||
return () => {
|
||||
this._runQuery().then(
|
||||
result => this._updateChild(result),
|
||||
error => this.showError(error)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private _getStorageKey(): string {
|
||||
return `insights.${this.insightConfig.cacheId}.${this.dashboardService.connectionManagementService.connectionInfo.connectionProfile.getOptionsKey()}`;
|
||||
}
|
||||
|
||||
private _runQuery(): Thenable<SimpleExecuteResult> {
|
||||
return this.dashboardService.queryManagementService.runQueryAndReturn(this.insightConfig.query as string).then(
|
||||
result => {
|
||||
return this._storeResult(result);
|
||||
},
|
||||
error => {
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateChild(result: SimpleExecuteResult): void {
|
||||
if (result.rowCount === 0) {
|
||||
this.showError(nls.localize('noResults', 'No results to show'));
|
||||
return;
|
||||
}
|
||||
|
||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory<IInsightsView>(insightRegistry.getCtorFromId(this._typeKey));
|
||||
this.componentHost.viewContainerRef.clear();
|
||||
|
||||
let componentRef = this.componentHost.viewContainerRef.createComponent(componentFactory);
|
||||
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.init) {
|
||||
componentInstance.init();
|
||||
}
|
||||
}
|
||||
|
||||
private _verifyConfig() {
|
||||
if (types.isUndefinedOrNull(this.insightConfig)) {
|
||||
throw new Error('Insight config must be defined');
|
||||
}
|
||||
|
||||
if (types.isUndefinedOrNull(this.insightConfig.type)) {
|
||||
throw new Error('An Insight type must be specified');
|
||||
}
|
||||
|
||||
if (Object.keys(this.insightConfig.type).length !== 1) {
|
||||
throw new Error('Exactly 1 insight type must be specified');
|
||||
}
|
||||
|
||||
if (!insightRegistry.getAllIds().includes(Object.keys(this.insightConfig.type)[0])) {
|
||||
throw new Error('The insight type must be a valid registered insight');
|
||||
}
|
||||
|
||||
if (!this.insightConfig.query && !this.insightConfig.queryFile) {
|
||||
throw new Error('No query was specified for this insight');
|
||||
}
|
||||
|
||||
if (!types.isStringArray(this.insightConfig.query)
|
||||
&& !types.isString(this.insightConfig.query)
|
||||
&& !types.isString(this.insightConfig.queryFile)) {
|
||||
throw new Error('Invalid query or queryfile specified');
|
||||
}
|
||||
}
|
||||
|
||||
private _parseConfig(): Thenable<void[]> {
|
||||
let promises: Array<Promise<void>> = [];
|
||||
|
||||
this._typeKey = Object.keys(this.insightConfig.type)[0];
|
||||
|
||||
if (types.isStringArray(this.insightConfig.query)) {
|
||||
this.insightConfig.query = this.insightConfig.query.join(' ');
|
||||
} else if (this.insightConfig.queryFile) {
|
||||
let filePath = this.insightConfig.queryFile;
|
||||
// check for workspace relative path
|
||||
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;
|
||||
}
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
pfs.readFile(filePath).then(
|
||||
buffer => {
|
||||
this.insightConfig.query = buffer.toString();
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { join } from 'path';
|
||||
|
||||
import { registerDashboardWidget, registerNonCustomDashboardWidget } from 'sql/platform/dashboard/common/widgetRegistry';
|
||||
import { Extensions as InsightExtensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { IInsightsConfig } from './interfaces';
|
||||
import { insightsContribution, insightsSchema } from 'sql/parts/dashboard/widgets/insights/insightsWidgetSchemas';
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(InsightExtensions.InsightContribution);
|
||||
|
||||
interface IInsightTypeContrib {
|
||||
id: string;
|
||||
contrib: IInsightsConfig;
|
||||
}
|
||||
|
||||
registerDashboardWidget('insights-widget', '', insightsSchema);
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IInsightTypeContrib | IInsightTypeContrib[]>('insights', [], insightsContribution).setHandler(extensions => {
|
||||
|
||||
function handleCommand(insight: IInsightTypeContrib, extension: IExtensionPointUser<any>) {
|
||||
|
||||
if (insight.contrib.queryFile) {
|
||||
insight.contrib.queryFile = join(extension.description.extensionFolderPath, insight.contrib.queryFile);
|
||||
}
|
||||
|
||||
if (insight.contrib.details && insight.contrib.details.queryFile) {
|
||||
insight.contrib.details.queryFile = join(extension.description.extensionFolderPath, insight.contrib.details.queryFile);
|
||||
}
|
||||
|
||||
registerNonCustomDashboardWidget(insight.id, '', insight.contrib);
|
||||
insightRegistry.registerExtensionInsight(insight.id, insight.contrib);
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IInsightTypeContrib>(value)) {
|
||||
for (let command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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: {
|
||||
type: 'string',
|
||||
description: nls.localize('insightIdDescription', 'Unique Identifier used for cacheing the results of the insight.')
|
||||
},
|
||||
type: {
|
||||
type: 'object',
|
||||
properties: insightRegistry.insightSchema.properties,
|
||||
minItems: 1,
|
||||
maxItems: 1
|
||||
},
|
||||
query: {
|
||||
type: ['string', 'array'],
|
||||
description: nls.localize('insightQueryDescription', 'SQL query to run. This should return exactly 1 resultset.')
|
||||
},
|
||||
queryFile: {
|
||||
type: 'string',
|
||||
description: nls.localize('insightQueryFileDescription', '[Optional] path to a file that contains a query. Use if "query" is not set')
|
||||
},
|
||||
details: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: ['string', 'array']
|
||||
},
|
||||
queryFile: {
|
||||
type: 'string'
|
||||
},
|
||||
value: {
|
||||
type: 'string'
|
||||
},
|
||||
label: {
|
||||
type: ['string', 'object'],
|
||||
properties: {
|
||||
column: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
},
|
||||
state: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
if: {
|
||||
type: 'string',
|
||||
enum: ['equals', 'notEquals', 'greaterThanOrEquals', 'greaterThan', 'lessThanOrEquals', 'lessThan', 'always']
|
||||
},
|
||||
equals: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
types: {
|
||||
type: 'object',
|
||||
properties: taskRegistry.taskSchemas
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionDatabaseDescription', 'Target database for the action; can use the format "${columnName} to use a data driven column name.')
|
||||
},
|
||||
server: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionServerDescription', 'Target server for the action; can use the format "${columnName} to use a data driven column name.')
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
description: nls.localize('actionUserDescription', 'Target user for the action; can use the format "${columnName} to use a data driven column name.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const insightType: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('carbon.extension.contributes.insightType.id', 'Identifier of the insight'),
|
||||
type: 'string'
|
||||
},
|
||||
contrib: insightsSchema
|
||||
}
|
||||
};
|
||||
|
||||
export const insightsContribution: IJSONSchema = {
|
||||
description: nls.localize('carbon.extension.contributes.insights', "Contributes insights to the dashboard palette."),
|
||||
oneOf: [
|
||||
insightType,
|
||||
{
|
||||
type: 'array',
|
||||
items: insightType
|
||||
}
|
||||
]
|
||||
};
|
||||
60
src/sql/parts/dashboard/widgets/insights/interfaces.ts
Normal file
60
src/sql/parts/dashboard/widgets/insights/interfaces.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IStateCondition {
|
||||
condition: {
|
||||
if: string,
|
||||
equals?: string
|
||||
};
|
||||
color?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface IInsightsLabel {
|
||||
column: string;
|
||||
icon?: string;
|
||||
state?: Array<IStateCondition>;
|
||||
}
|
||||
|
||||
export interface IInsightsConfigDetails {
|
||||
query?: string | Array<string>;
|
||||
queryFile?: string;
|
||||
label?: string | IInsightsLabel;
|
||||
value?: string;
|
||||
actions?: {
|
||||
types: Array<string>;
|
||||
database?: string;
|
||||
server?: string;
|
||||
user?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IInsightData {
|
||||
columns: Array<string>;
|
||||
rows: Array<Array<string>>;
|
||||
}
|
||||
|
||||
export interface IInsightsView {
|
||||
data: IInsightData;
|
||||
config?: { [key: string]: any };
|
||||
init?: () => void;
|
||||
}
|
||||
|
||||
export interface ISize {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IInsightsConfig {
|
||||
cacheId?: string;
|
||||
type: any;
|
||||
name?: string;
|
||||
provider?: string;
|
||||
edition?: number | Array<number>;
|
||||
gridItemConfig?: ISize;
|
||||
query?: string | Array<string>;
|
||||
queryFile?: string;
|
||||
details?: IInsightsConfigDetails;
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { BaseChartDirective } from 'ng2-charts/ng2-charts';
|
||||
|
||||
/* SQL Imports */
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
|
||||
import { IInsightsView, IInsightData } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { memoize, unmemoize } from 'sql/base/common/decorators';
|
||||
|
||||
/* VS Imports */
|
||||
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 { IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
export enum ChartType {
|
||||
Bar = 'bar',
|
||||
Doughnut = 'doughnut',
|
||||
HorizontalBar = 'horizontalBar',
|
||||
Line = 'line',
|
||||
Pie = 'pie',
|
||||
TimeSeries = 'timeSeries',
|
||||
Scatter = 'scatter'
|
||||
}
|
||||
|
||||
export enum DataDirection {
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal'
|
||||
}
|
||||
|
||||
export enum LegendPosition {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
export function customMixin(destination: any, source: any, overwrite?: boolean): any {
|
||||
if (types.isObject(source)) {
|
||||
mixin(destination, source, overwrite, customMixin);
|
||||
} else if (types.isArray(source)) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (destination[i]) {
|
||||
mixin(destination[i], source[i], overwrite, customMixin);
|
||||
} else {
|
||||
destination[i] = source[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destination = source;
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export interface IDataSet {
|
||||
data: Array<number>;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IPointDataSet {
|
||||
data: Array<{ x: number | string, y: number }>;
|
||||
label?: string;
|
||||
fill: boolean;
|
||||
backgroundColor?: Color;
|
||||
}
|
||||
|
||||
export interface IChartConfig {
|
||||
colorMap?: { [column: string]: string };
|
||||
labelFirstColumn?: boolean;
|
||||
legendPosition?: LegendPosition;
|
||||
dataDirection?: DataDirection;
|
||||
columnsAsLabels?: boolean;
|
||||
}
|
||||
|
||||
export const defaultChartConfig: IChartConfig = {
|
||||
labelFirstColumn: false,
|
||||
columnsAsLabels: false,
|
||||
legendPosition: LegendPosition.Top,
|
||||
dataDirection: DataDirection.Vertical
|
||||
};
|
||||
|
||||
@Component({
|
||||
template: ` <div style="display: block; width: 100%; height: 100%; position: relative">
|
||||
<canvas #canvas *ngIf="_isDataAvailable"
|
||||
baseChart
|
||||
[datasets]="chartData"
|
||||
[labels]="labels"
|
||||
[chartType]="chartType"
|
||||
[colors]="colors"
|
||||
[options]="_options"></canvas>
|
||||
</div>`
|
||||
})
|
||||
export abstract class ChartInsight implements IInsightsView, OnDestroy {
|
||||
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;
|
||||
|
||||
protected abstract get chartType(): ChartType;
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
init() {
|
||||
this._disposables.push(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
|
||||
// hence it's easier to not render until ready
|
||||
this.options = mixin(this.options, { maintainAspectRatio: false });
|
||||
this._isDataAvailable = true;
|
||||
this._changeRef.detectChanges();
|
||||
TelemetryUtils.addTelemetry(this._bootstrapService.telemetryService, TelemetryKeys.ChartCreated, { type: this.chartType });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options for the chart; handles rerendering the chart if needed
|
||||
*/
|
||||
public set options(options: any) {
|
||||
this._options = options;
|
||||
if (this._isDataAvailable) {
|
||||
this._options = mixin({}, mixin(this._options, { animation: { duration: 0 } }));
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public get options(): any {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
protected updateTheme(e: IColorTheme): void {
|
||||
let options = {
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
}
|
||||
}
|
||||
};
|
||||
this.options = mixin({}, mixin(this.options, options));
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
// cheaper refresh but causes problems when change data for rerender
|
||||
this._chart.ngOnChanges({});
|
||||
}
|
||||
|
||||
public getCanvasData(): string {
|
||||
if (this._chart && this._chart.chart) {
|
||||
return this._chart.chart.toBase64Image();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@Input() set data(data: IInsightData) {
|
||||
// unmemoize chart data as the data needs to be recalced
|
||||
unmemoize(this, 'chartData');
|
||||
unmemoize(this, 'labels');
|
||||
this._data = data;
|
||||
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
protected clearMemoize(): void {
|
||||
// unmemoize getters since their result can be changed by a new config
|
||||
unmemoize(this, 'getChartData');
|
||||
unmemoize(this, 'getLabels');
|
||||
unmemoize(this, 'colors');
|
||||
}
|
||||
|
||||
@Input() set config(config: IChartConfig) {
|
||||
this.clearMemoize();
|
||||
this._config = mixin(config, this._defaultConfig, false);
|
||||
this.legendPosition = this._config.legendPosition;
|
||||
if (this._isDataAvailable) {
|
||||
this._options = mixin({}, mixin(this._options, { animation: false }));
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/* Typescript does not allow you to access getters/setters for super classes.
|
||||
This is a workaround that allows us to still call base getter */
|
||||
@memoize
|
||||
protected getChartData(): Array<IDataSet> {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
if (this._config.labelFirstColumn) {
|
||||
return this._data.rows.map((row) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)).slice(1),
|
||||
label: row[0]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return this._data.rows.map((row, i) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)),
|
||||
label: 'Series' + i
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this._config.columnsAsLabels) {
|
||||
return this._data.rows[0].map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i])),
|
||||
label: this._data.columns[i]
|
||||
};
|
||||
}).slice(1);
|
||||
} else {
|
||||
return this._data.rows[0].map((row, i) => {
|
||||
return {
|
||||
data: this._data.rows.map(row => Number(row[i])),
|
||||
label: 'Series' + i
|
||||
};
|
||||
}).slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get chartData(): Array<IDataSet | IPointDataSet> {
|
||||
return this.getChartData();
|
||||
}
|
||||
|
||||
@memoize
|
||||
public getLabels(): Array<string> {
|
||||
if (this._config.dataDirection === 'horizontal') {
|
||||
return this._data.columns;
|
||||
} else {
|
||||
return this._data.rows.map(row => row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
return this.getLabels();
|
||||
}
|
||||
|
||||
|
||||
@memoize
|
||||
private get colors(): { backgroundColor: string[] }[] {
|
||||
if (this._config && this._config.colorMap) {
|
||||
let backgroundColor = this.labels.map((item) => {
|
||||
return this._config.colorMap[item];
|
||||
});
|
||||
let colorsMap = { backgroundColor };
|
||||
return [colorsMap];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set legendPosition(input: LegendPosition) {
|
||||
let options = {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
};
|
||||
if (input === 'none') {
|
||||
options.legend.display = false;
|
||||
} else {
|
||||
options.legend.position = input;
|
||||
}
|
||||
this.options = mixin(this.options, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export const chartInsightSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('chartInsightDescription', 'Displays results of a query as a chart on the dashboard'),
|
||||
properties: {
|
||||
colorMap: {
|
||||
type: 'object',
|
||||
description: nls.localize('colorMapDescription', 'Maps "column name" -> color. for example add "column1": red to ensure this column uses a red color ')
|
||||
},
|
||||
legendPosition: {
|
||||
type: 'string',
|
||||
description: nls.localize('legendDescription', 'Indicates preferred position and visibility of the chart legend. These are the column names from your query, and map to the label of each chart entry'),
|
||||
default: 'none',
|
||||
enum: ['top', 'bottom', 'left', 'right', 'none']
|
||||
},
|
||||
labelFirstColumn: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('labelFirstColumnDescription', 'If dataDirection is horizontal, setting this to true uses the first columns value for the legend.'),
|
||||
default: false
|
||||
},
|
||||
columnsAsLabels: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('columnsAsLabels', 'If dataDirection is vertical, setting this to true will use the columns names for the legend.'),
|
||||
default: false
|
||||
},
|
||||
dataDirection: {
|
||||
type: 'string',
|
||||
description: nls.localize('dataDirectionDescription', 'Defines whether the data is read from a column (vertical) or a row (horizontal). For time series this is ignored as direction must be vertical.'),
|
||||
default: 'vertical',
|
||||
enum: ['vertical', 'horizontal'],
|
||||
enumDescriptions: ['When vertical, the first column is used to define the x-axis labels, with other columns expected to be numerical.', 'When horizontal, the column names are used as the x-axis labels.']
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartInsight, ChartType, customMixin } 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 default class BarChart extends ChartInsight {
|
||||
protected readonly chartType: ChartType = ChartType.Bar;
|
||||
|
||||
protected updateTheme(e: IColorTheme): void {
|
||||
super.updateTheme(e);
|
||||
let options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
ticks: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
gridLines: {
|
||||
color: e.getColor(editorLineNumbers)
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
scaleLabel: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
ticks: {
|
||||
fontColor: e.getColor(colors.editorForeground)
|
||||
},
|
||||
gridLines: {
|
||||
color: e.getColor(editorLineNumbers)
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = mixin({}, mixin(this.options, options, true, customMixin));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
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 BarChart from './barChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const barSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('bar', '', barSchema, BarChart);
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import PieChart from './pieChart.component';
|
||||
|
||||
export default class DoughnutChart extends PieChart {
|
||||
protected readonly chartType: ChartType = ChartType.Doughnut;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
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 DoughnutChart from './doughnutChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const doughnutChartSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('doughnut', '', doughnutChartSchema, DoughnutChart);
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChartType } from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
import BarChart from './barChart.component';
|
||||
|
||||
export default class HorizontalBarChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.HorizontalBar;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
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 HorizontalBarChart from './horizontalBarChart.component';
|
||||
|
||||
const properties: IJSONSchema = {
|
||||
|
||||
};
|
||||
|
||||
const horizontalBarSchema = mixin(clone(chartInsightSchema), properties) as IJSONSchema;
|
||||
|
||||
registerInsight('horizontalBar', '', horizontalBarSchema, HorizontalBarChart);
|
||||
@@ -0,0 +1,98 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { memoize, unmemoize } from 'sql/base/common/decorators';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { clone } from 'vs/base/common/objects';
|
||||
|
||||
export enum DataType {
|
||||
Number = 'number',
|
||||
Point = 'point'
|
||||
}
|
||||
|
||||
export interface ILineConfig extends IChartConfig {
|
||||
dataType?: DataType;
|
||||
}
|
||||
|
||||
const defaultLineConfig = mixin(clone(defaultChartConfig), { dataType: 'number' }) as ILineConfig;
|
||||
|
||||
export default class LineChart extends BarChart {
|
||||
protected readonly chartType: ChartType = ChartType.Line;
|
||||
protected _config: ILineConfig;
|
||||
protected _defaultConfig = defaultLineConfig;
|
||||
|
||||
public init() {
|
||||
if (this._config.dataType === DataType.Point) {
|
||||
this.addAxisLabels();
|
||||
}
|
||||
super.init();
|
||||
}
|
||||
|
||||
public get chartData(): Array<IDataSet | IPointDataSet> {
|
||||
if (this._config.dataType === DataType.Number) {
|
||||
return super.getChartData();
|
||||
} else {
|
||||
return this.getDataAsPoint();
|
||||
}
|
||||
}
|
||||
|
||||
protected clearMemoize() {
|
||||
super.clearMemoize();
|
||||
unmemoize(this, 'getDataAsPoint');
|
||||
}
|
||||
|
||||
@memoize
|
||||
protected getDataAsPoint(): Array<IPointDataSet> {
|
||||
let dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
let legend = row[0];
|
||||
if (!dataSetMap[legend]) {
|
||||
dataSetMap[legend] = { label: legend, data: [], fill: false };
|
||||
}
|
||||
dataSetMap[legend].data.push({ x: Number(row[1]), y: Number(row[2]) });
|
||||
}
|
||||
});
|
||||
return Object.values(dataSetMap);
|
||||
}
|
||||
|
||||
public get labels(): Array<string> {
|
||||
if (this._config.dataType === DataType.Number) {
|
||||
return super.getLabels();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected addAxisLabels(): void {
|
||||
let xLabel = this._data.columns[1] || 'x';
|
||||
let yLabel = this._data.columns[2] || 'y';
|
||||
let options = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'linear',
|
||||
position: 'bottom',
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: xLabel
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: yLabel,
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
this.options = mixin(this.options, options, true, customMixin);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user