Initial Code Layering (#3788)

* working on formatting

* fixed basic lint errors; starting moving things to their appropriate location

* formatting

* update tslint to match the version of vscode we have

* remove unused code

* work in progress fixing layering

* formatting

* moved connection management service to platform

* formatting

* add missing file

* moving more servies

* formatting

* moving more services

* formatting

* wip

* moving more services

* formatting

* revert back tslint rules

* move css file

* add missing svgs
This commit is contained in:
Anthony Dresser
2019-01-25 14:52:35 -08:00
committed by GitHub
parent c8986464ec
commit ea67859de7
338 changed files with 2036 additions and 7386 deletions

View File

@@ -0,0 +1,238 @@
/*---------------------------------------------------------------------------------------------
* 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 { attachListStyler } from 'vs/platform/theme/common/styler';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import * as sqlops from 'sqlops';
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/platform/accountManagement/common/accountPickerViewModel';
export class AccountPicker extends Disposable {
public static ACCOUNTPICKERLIST_HEIGHT = 47;
public viewModel: AccountPickerViewModel;
private _accountList: List<sqlops.Account>;
private _rootElement: HTMLElement;
private _refreshContainer: HTMLElement;
private _listContainer: HTMLElement;
private _dropdown: DropdownList;
private _refreshAccountAction: RefreshAccountAction;
// EVENTING ////////////////////////////////////////////////////////////
private _addAccountCompleteEmitter: Emitter<void>;
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
private _addAccountErrorEmitter: Emitter<string>;
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
private _addAccountStartEmitter: Emitter<void>;
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
private _onAccountSelectionChangeEvent: Emitter<sqlops.Account>;
public get onAccountSelectionChangeEvent(): Event<sqlops.Account> { return this._onAccountSelectionChangeEvent.event; }
constructor(
private _providerId: string,
@IThemeService private _themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IContextViewService private _contextViewService: IContextViewService
) {
super();
// Create event emitters
this._addAccountCompleteEmitter = new Emitter<void>();
this._addAccountErrorEmitter = new Emitter<string>();
this._addAccountStartEmitter = new Emitter<void>();
this._onAccountSelectionChangeEvent = new Emitter<sqlops.Account>();
// Create the view model, wire up the events, and initialize with baseline data
this.viewModel = this._instantiationService.createInstance(AccountPickerViewModel, this._providerId);
this.viewModel.updateAccountListEvent(arg => {
if (arg.providerId === this._providerId) {
this.updateAccountList(arg.accountList);
}
});
}
// PUBLIC METHODS //////////////////////////////////////////////////////
/**
* Render account picker
*/
public render(container: HTMLElement): void {
DOM.append(container, this._rootElement);
}
// PUBLIC METHODS //////////////////////////////////////////////////////
/**
* Create account picker component
*/
public createAccountPickerComponent() {
// Create an account list
let delegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT);
let accountRenderer = new AccountPickerListRenderer();
this._listContainer = DOM.$('div.account-list-container');
this._accountList = new List<sqlops.Account>(this._listContainer, delegate, [accountRenderer]);
this._register(attachListStyler(this._accountList, this._themeService));
this._rootElement = DOM.$('div.account-picker-container');
// Create a dropdown for account picker
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<sqlops.Account>) => {
if (e.elements.length === 1) {
this._dropdown.renderLabel();
this.onAccountSelectionChange(e.elements[0]);
}
}));
// Create refresh account action
this._refreshContainer = DOM.append(this._rootElement, DOM.$('div.refresh-container'));
DOM.append(this._refreshContainer, DOM.$('div.sql icon warning'));
let actionBar = new ActionBar(this._refreshContainer, { animated: false });
this._refreshAccountAction = this._instantiationService.createInstance(RefreshAccountAction);
actionBar.push(this._refreshAccountAction, { icon: false, label: true });
if (this._accountList.length > 0) {
this._accountList.setSelection([0]);
this.onAccountSelectionChange(this._accountList.getSelectedElements()[0]);
} else {
new Builder(this._refreshContainer).hide();
}
this._register(this._themeService.onThemeChange(e => this.updateTheme(e)));
this.updateTheme(this._themeService.getTheme());
// Load the initial contents of the view model
this.viewModel.initialize()
.then((accounts: sqlops.Account[]) => {
this.updateAccountList(accounts);
});
}
public dispose() {
super.dispose();
if (this._accountList) {
this._accountList.dispose();
}
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private onAccountSelectionChange(account: sqlops.Account) {
this.viewModel.selectedAccount = account;
if (account && account.isStale) {
this._refreshAccountAction.account = account;
new Builder(this._refreshContainer).show();
} else {
new Builder(this._refreshContainer).hide();
}
this._onAccountSelectionChangeEvent.fire(account);
}
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'));
// Set the account icon
icon.classList.add('icon', account.displayInfo.accountType);
// TODO: Pick between the light and dark logo
label.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.contextualDisplayName + ')';
if (account.isStale) {
badgeContent.className = 'badge-content 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: sqlops.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: ITheme): 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;
}
}
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import * as sqlops from 'sqlops';
import { IAccountPickerService } from 'sql/platform/accountManagement/common/accountPicker';
import { AccountPicker } from 'sql/platform/accountManagement/browser/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<sqlops.Account>;
public get onAccountSelectionChangeEvent(): Event<sqlops.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<sqlops.Account>();
}
/**
* Get selected account
*/
public get selectedAccount(): sqlops.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.createAccountPickerComponent();
}
this._accountPicker.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());
this._accountPicker.addAccountErrorEvent((msg) => this._addAccountErrorEmitter.fire(msg));
this._accountPicker.addAccountStartEvent(() => this._addAccountStartEmitter.fire());
this._accountPicker.onAccountSelectionChangeEvent((account) => this._onAccountSelectionChangeEvent.fire(account));
this._accountPicker.render(container);
}
}

View File

@@ -0,0 +1,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;
}

View 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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import * as sqlops from 'sqlops';
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<sqlops.Account>;
selectedAccount: sqlops.Account;
}

View File

@@ -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 sqlops from 'sqlops';
import { Event, Emitter } from 'vs/base/common/event';
import { IAccountManagementService } from 'sql/platform/accountManagement/common/interfaces';
import { UpdateAccountListEventParams } from 'sql/platform/accountManagement/common/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: sqlops.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<sqlops.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 [];
});
}
}

View File

@@ -0,0 +1,204 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { AccountAdditionResult } from 'sql/platform/accountManagement/common/eventTypes';
import { IAccountStore } from 'sql/platform/accountManagement/common/interfaces';
export default class AccountStore implements IAccountStore {
// CONSTANTS ///////////////////////////////////////////////////////////
public static MEMENTO_KEY: string = 'Microsoft.SqlTools.Accounts';
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _activeOperation: Thenable<any>;
constructor(private _memento: object) { }
// PUBLIC METHODS //////////////////////////////////////////////////////
public addOrUpdate(newAccount: sqlops.Account): Thenable<AccountAdditionResult> {
let self = this;
return this.doOperation(() => {
return self.readFromMemento()
.then(accounts => {
// Determine if account exists and proceed accordingly
let match = accounts.findIndex(account => AccountStore.findAccountByKey(account.key, newAccount.key));
return match < 0
? self.addToAccountList(accounts, newAccount)
: self.updateAccountList(accounts, newAccount.key, (matchAccount) => { AccountStore.mergeAccounts(newAccount, matchAccount); });
})
.then(result => self.writeToMemento(result.updatedAccounts).then(() => result))
.then(result => <AccountAdditionResult>result);
});
}
public getAccountsByProvider(providerId: string): Thenable<sqlops.Account[]> {
let self = this;
return this.doOperation(() => {
return self.readFromMemento()
.then(accounts => accounts.filter(account => account.key.providerId === providerId));
});
}
public getAllAccounts(): Thenable<sqlops.Account[]> {
let self = this;
return this.doOperation(() => {
return self.readFromMemento();
});
}
public remove(key: sqlops.AccountKey): Thenable<boolean> {
let self = this;
return this.doOperation(() => {
return self.readFromMemento()
.then(accounts => self.removeFromAccountList(accounts, key))
.then(result => self.writeToMemento(result.updatedAccounts).then(() => result))
.then(result => result.accountRemoved);
});
}
public update(key: sqlops.AccountKey, updateOperation: (account: sqlops.Account) => void): Thenable<boolean> {
let self = this;
return this.doOperation(() => {
return self.readFromMemento()
.then(accounts => self.updateAccountList(accounts, key, updateOperation))
.then(result => self.writeToMemento(result.updatedAccounts).then(() => result))
.then(result => result.accountModified);
});
}
// PRIVATE METHODS /////////////////////////////////////////////////////
private static findAccountByKey(key1: sqlops.AccountKey, key2: sqlops.AccountKey): boolean {
// Provider ID and Account ID must match
return key1.providerId === key2.providerId && key1.accountId === key2.accountId;
}
private static mergeAccounts(source: sqlops.Account, target: sqlops.Account): void {
// Take any display info changes
target.displayInfo = source.displayInfo;
// Take all property changes
target.properties = source.properties;
// Take any stale changes
target.isStale = source.isStale;
}
private doOperation<T>(op: () => Thenable<T>) {
// Initialize the active operation to an empty promise if necessary
let activeOperation = this._activeOperation || Promise.resolve<any>(null);
// Chain the operation to perform to the end of the existing promise
activeOperation = activeOperation.then(op);
// Add a catch at the end to make sure we can continue after any errors
activeOperation = activeOperation.then(null, (err) => {
// TODO: Log the error
});
// Point the current active operation to this one
this._activeOperation = activeOperation;
return <Promise<T>>this._activeOperation;
}
private addToAccountList(accounts: sqlops.Account[], accountToAdd: sqlops.Account): AccountListOperationResult {
// Check if the entry already exists
let match = accounts.findIndex(account => AccountStore.findAccountByKey(account.key, accountToAdd.key));
if (match >= 0) {
// Account already exists, we won't do anything
return {
accountAdded: false,
accountModified: false,
accountRemoved: false,
changedAccount: null,
updatedAccounts: accounts
};
}
// Add the account to the store
accounts.push(accountToAdd);
return {
accountAdded: true,
accountModified: false,
accountRemoved: false,
changedAccount: accountToAdd,
updatedAccounts: accounts
};
}
private removeFromAccountList(accounts: sqlops.Account[], accountToRemove: sqlops.AccountKey): AccountListOperationResult {
// Check if the entry exists
let match = accounts.findIndex(account => AccountStore.findAccountByKey(account.key, accountToRemove));
if (match >= 0) {
// Account exists, remove it from the account list
accounts.splice(match, 1);
}
return {
accountAdded: false,
accountModified: false,
accountRemoved: match >= 0,
changedAccount: null,
updatedAccounts: accounts
};
}
private updateAccountList(accounts: sqlops.Account[], accountToUpdate: sqlops.AccountKey, updateOperation: (account: sqlops.Account) => void): AccountListOperationResult {
// Check if the entry exists
let match = accounts.findIndex(account => AccountStore.findAccountByKey(account.key, accountToUpdate));
if (match < 0) {
// Account doesn't exist, we won't do anything
return {
accountAdded: false,
accountModified: false,
accountRemoved: false,
changedAccount: null,
updatedAccounts: accounts
};
}
// Account exists, apply the update operation to it
updateOperation(accounts[match]);
return {
accountAdded: false,
accountModified: true,
accountRemoved: false,
changedAccount: accounts[match],
updatedAccounts: accounts
};
}
// MEMENTO IO METHODS //////////////////////////////////////////////////
private readFromMemento(): Thenable<sqlops.Account[]> {
// Initialize the account list if it isn't already
let accounts = this._memento[AccountStore.MEMENTO_KEY];
if (!accounts) {
accounts = [];
}
// Make a deep copy of the account list to ensure that the memento list isn't obliterated
accounts = JSON.parse(JSON.stringify(accounts));
return Promise.resolve(accounts);
}
private writeToMemento(accounts: sqlops.Account[]): Thenable<void> {
// Store a shallow copy of the account list to disconnect the memento list from the active list
this._memento[AccountStore.MEMENTO_KEY] = JSON.parse(JSON.stringify(accounts));
return Promise.resolve();
}
}
interface AccountListOperationResult extends AccountAdditionResult {
accountRemoved: boolean;
updatedAccounts: sqlops.Account[];
}

View 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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
/**
* Result from calling add/update on the account store
*/
export interface AccountAdditionResult {
/**
* Whether or not an account was added in the add/update process
*/
accountAdded: boolean;
/**
* Whether or not an account was updated in the add/update process
*/
accountModified: boolean;
/**
* The account that was added/updated (with any updates applied)
*/
changedAccount: sqlops.Account;
}
/**
* Parameters that go along with an account provider being added
*/
export interface AccountProviderAddedEventParams {
/**
* The provider that was registered
*/
addedProvider: sqlops.AccountProviderMetadata;
/**
* The accounts that were rehydrated with the provider
*/
initialAccounts: sqlops.Account[];
}
/**
* Parameters that go along when a provider's account list changes
*/
export interface UpdateAccountListEventParams {
/**
* ID of the provider who's account list changes
*/
providerId: string;
/**
* Updated list of accounts, sorted appropriately
*/
accountList: sqlops.Account[];
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { Event } from 'vs/base/common/event';
import { AccountAdditionResult, AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/platform/accountManagement/common/eventTypes';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const SERVICE_ID = 'accountManagementService';
export const IAccountManagementService = createDecorator<IAccountManagementService>(SERVICE_ID);
export interface IAccountManagementService {
_serviceBrand: any;
// ACCOUNT MANAGEMENT METHODS //////////////////////////////////////////
accountUpdated(account: sqlops.Account): Thenable<void>;
addAccount(providerId: string): Thenable<void>;
getAccountProviderMetadata(): Thenable<sqlops.AccountProviderMetadata[]>;
getAccountsForProvider(providerId: string): Thenable<sqlops.Account[]>;
getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}>;
removeAccount(accountKey: sqlops.AccountKey): Thenable<boolean>;
refreshAccount(account: sqlops.Account): Thenable<sqlops.Account>;
// UI METHODS //////////////////////////////////////////////////////////
openAccountListDialog(): Thenable<void>;
beginAutoOAuthDeviceCode(providerId: string, title: string, message: string, userCode: string, uri: string): Thenable<void>;
endAutoOAuthDeviceCode(): void;
cancelAutoOAuthDeviceCode(providerId: string): void;
copyUserCodeAndOpenBrowser(userCode: string, uri: string): void;
// SERVICE MANAGEMENT METHODS /////////////////////////////////////////
registerProvider(providerMetadata: sqlops.AccountProviderMetadata, provider: sqlops.AccountProvider): void;
shutdown(): void;
unregisterProvider(providerMetadata: sqlops.AccountProviderMetadata): void;
// EVENTING ////////////////////////////////////////////////////////////
readonly addAccountProviderEvent: Event<AccountProviderAddedEventParams>;
readonly removeAccountProviderEvent: Event<sqlops.AccountProviderMetadata>;
readonly updateAccountListEvent: Event<UpdateAccountListEventParams>;
}
// Enum matching the AzureResource enum from sqlops.d.ts
export enum AzureResource {
ResourceManagement = 0,
Sql = 1
}
export interface IAccountStore {
/**
* Adds the provided account if the account doesn't exist. Updates the account if it already exists
* @param {Account} account Account to add/update
* @return {Thenable<AccountAdditionResult>} Results of the add/update operation
*/
addOrUpdate(account: sqlops.Account): Thenable<AccountAdditionResult>;
/**
* Retrieves all accounts, filtered by provider ID
* @param {string} providerId ID of the provider to filter by
* @return {Thenable<Account[]>} Promise to return all accounts that belong to the provided provider
*/
getAccountsByProvider(providerId: string): Thenable<sqlops.Account[]>;
/**
* Retrieves all accounts in the store. Returns empty array if store is not initialized
* @return {Thenable<Account[]>} Promise to return all accounts
*/
getAllAccounts(): Thenable<sqlops.Account[]>;
/**
* Removes an account.
* Returns false if the account was not found.
* Otherwise, returns true.
* @param key - The key of an account.
* @returns True if the account was removed, false if the account doesn't exist
*/
remove(key: sqlops.AccountKey): Thenable<boolean>;
/**
* Updates the custom properties stored with an account.
* Returns null if no account was found to update.
* Otherwise, returns a new updated account instance.
* @param key - The key of an account.
* @param updateOperation - Operation to perform on the matching account
* @returns True if the account was modified, false if the account doesn't exist
*/
update(key: sqlops.AccountKey, updateOperation: (account: sqlops.Account) => void): Thenable<boolean>;
}

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Subscription } from 'rxjs/Subscription';
const ANGULAREVENTING_SERVICE_ID = 'angularEventingService';
export const IAngularEventingService = createDecorator<IAngularEventingService>(ANGULAREVENTING_SERVICE_ID);
export enum AngularEventType {
NAV_DATABASE,
NAV_SERVER,
DELETE_WIDGET,
PINUNPIN_TAB,
NEW_TABS,
CLOSE_TAB,
COLLAPSE_WIDGET
}
export interface IDeleteWidgetPayload {
id: string;
}
export interface IAngularEvent {
event: AngularEventType;
payload: any;
}
export interface IAngularEventingService {
_serviceBrand: any;
/**
* Adds a listener for the dashboard to send events, should only be called once for each dashboard by the dashboard itself
* @param uri Uri of the dashboard
* @param cb Listening function
* @returns
*/
onAngularEvent(uri: string, cb: (event: IAngularEvent) => void): Subscription;
/**
* Send an event to the dashboard; no op if the dashboard has not started listening yet
* @param uri Uri of the dashboard to send the event to
* @param event event to send
*/
sendAngularEvent(uri: string, event: AngularEventType, payload?: any): void;
}

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import { warn } from 'sql/base/common/log';
import { IAngularEventingService, IAngularEvent, AngularEventType } from 'sql/platform/angularEventing/common/angularEventingService';
export class AngularEventingService implements IAngularEventingService {
public _serviceBrand: any;
private _angularMap = new Map<string, Subject<IAngularEvent>>();
public onAngularEvent(uri: string, cb: (event: IAngularEvent) => void): Subscription {
let subject: Subject<IAngularEvent>;
if (!this._angularMap.has(uri)) {
subject = new Subject<IAngularEvent>();
this._angularMap.set(uri, subject);
} else {
subject = this._angularMap.get(uri);
}
let sub = subject.subscribe(cb);
return sub;
}
public sendAngularEvent(uri: string, event: AngularEventType, payload?: any): void {
if (!this._angularMap.has(uri)) {
warn('Got request to send an event to a dashboard that has not started listening');
} else {
this._angularMap.get(uri).next({ event, payload });
}
}
}

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import * as sqlops from 'sqlops';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
export enum TaskExecutionMode {
execute = 0,
script = 1,
executeAndScript = 2,
}
export const SERVICE_ID = 'backupService';
export const UI_SERVICE_ID = 'backupUiService';
export const IBackupUiService = createDecorator<IBackupUiService>(UI_SERVICE_ID);
export interface IBackupUiService {
_serviceBrand: any;
/**
* Show backup wizard
*/
showBackup(connection: IConnectionProfile): Promise<any>;
/**
* On show backup event
*/
onShowBackupEvent: Event<{ connection: IConnectionProfile, ownerUri: string }>;
/**
* Close backup wizard
*/
closeBackup();
/**
* After the backup dialog is rendered, run Modal methods to set focusable elements, etc.
*/
onShowBackupDialog();
}
export const IBackupService = createDecorator<IBackupService>(SERVICE_ID);
export interface IBackupService {
_serviceBrand: any;
getBackupConfigInfo(connectionUri: string): Thenable<sqlops.BackupConfigInfo>;
/**
* Backup a data source using the provided connection
*/
backup(connectionUri: string, backupInfo: { [key: string]: any }, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.BackupResponse>;
/**
* Register a disaster recovery provider
*/
registerProvider(providerId: string, provider: sqlops.BackupProvider): void;
}

View File

@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import * as sqlops from 'sqlops';
import * as Constants from 'sql/common/constants';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IBackupService, TaskExecutionMode } from 'sql/platform/backup/common/backupService';
export class BackupService implements IBackupService {
public _serviceBrand: any;
private _providers: { [handle: string]: sqlops.BackupProvider; } = Object.create(null);
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
}
/**
* Get database metadata needed to populate backup UI
*/
public getBackupConfigInfo(connectionUri: string): Thenable<sqlops.BackupConfigInfo> {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
let provider = this._providers[providerId];
if (provider) {
return provider.getBackupConfigInfo(connectionUri);
}
}
return Promise.resolve(undefined);
}
/**
* Backup a data source using the provided connection
*/
public backup(connectionUri: string, backupInfo: { [key: string]: any }, taskExecutionMode: TaskExecutionMode): Thenable<sqlops.BackupResponse> {
return new Promise<sqlops.BackupResponse>((resolve, reject) => {
let providerResult = this.getProvider(connectionUri);
if (providerResult) {
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.BackupCreated, { provider: providerResult.providerName });
providerResult.provider.backup(connectionUri, backupInfo, taskExecutionMode).then(result => {
resolve(result);
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
private getProvider(connectionUri: string): { provider: sqlops.BackupProvider, providerName: string } {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
return { provider: this._providers[providerId], providerName: providerId };
} else {
return undefined;
}
}
/**
* Register a disaster recovery provider
*/
public registerProvider(providerId: string, provider: sqlops.BackupProvider): void {
this._providers[providerId] = provider;
}
}

View File

@@ -0,0 +1,238 @@
/*---------------------------------------------------------------------------------------------
* 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 'sql/platform/connection/common/connectionManagementInfo';
import * as Constants from 'sql/common/constants';
import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions as ConnectionExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
import { toObject } from 'sql/base/common/map';
import * as sqlops from 'sqlops';
import { Event, Emitter } from 'vs/base/common/event';
import { IAction } from 'vs/base/common/actions';
import { Memento } from 'vs/workbench/common/memento';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Disposable } from 'vs/base/common/lifecycle';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
export const SERVICE_ID = 'capabilitiesService';
export const HOST_NAME = 'sqlops';
export const HOST_VERSION = '1.0';
const connectionRegistry = Registry.as<IConnectionProviderRegistry>(ConnectionExtensions.ConnectionProviderContributions);
interface ConnectionCache {
[id: string]: ConnectionProviderProperties;
}
interface CapabilitiesMomento {
connectionProviderCache: ConnectionCache;
}
export const clientCapabilities = {
hostName: HOST_NAME,
hostVersion: HOST_VERSION
};
export interface ProviderFeatures {
connection: ConnectionProviderProperties;
}
export const ICapabilitiesService = createDecorator<ICapabilitiesService>(SERVICE_ID);
/**
* Interface for managing provider capabilities
*/
export interface ICapabilitiesService {
_serviceBrand: any;
/**
* Retrieve a list of registered capabilities providers
*/
getCapabilities(provider: string): ProviderFeatures;
/**
* get the old version of provider information
*/
getLegacyCapabilities(provider: string): sqlops.DataProtocolServerCapabilities;
/**
* Register a capabilities provider
*/
registerProvider(provider: sqlops.CapabilitiesProvider): void;
/**
* Returns true if the feature is available for given connection
*/
isFeatureAvailable(action: IAction, connectionManagementInfo: ConnectionManagementInfo): boolean;
/**
* When a new capabilities is registered, it emits the provider name, be to use to get the new capabilities
*/
readonly onCapabilitiesRegistered: Event<ProviderFeatures>;
/**
* Get an array of all known providers
*/
readonly providers: { [id: string]: ProviderFeatures };
}
/**
* Capabilities service implementation class. This class provides the ability
* to discover the DMP capabilties that a DMP provider offers.
*/
export class CapabilitiesService extends Disposable implements ICapabilitiesService {
_serviceBrand: any;
private _momento = new Memento('capabilities');
private _providers = new Map<string, ProviderFeatures>();
private _featureUpdateEvents = new Map<string, Emitter<ProviderFeatures>>();
private _legacyProviders = new Map<string, sqlops.DataProtocolServerCapabilities>();
private _onCapabilitiesRegistered = this._register(new Emitter<ProviderFeatures>());
public readonly onCapabilitiesRegistered = this._onCapabilitiesRegistered.event;
constructor(
@IStorageService private _storageService: IStorageService,
@IExtensionService extensionService: IExtensionService,
@IExtensionManagementService extentionManagementService: IExtensionManagementService
) {
super();
if (!this.capabilities.connectionProviderCache) {
this.capabilities.connectionProviderCache = {};
}
// handle in case some extensions have already registered (unlikley)
Object.entries(connectionRegistry.providers).map(v => {
this.handleConnectionProvider({ id: v[0], properties: v[1] });
});
// register for when new extensions are added
this._register(connectionRegistry.onNewProvider(this.handleConnectionProvider, this));
// handle adding already known capabilities (could have caching problems)
Object.entries(this.capabilities.connectionProviderCache).map(v => {
this.handleConnectionProvider({ id: v[0], properties: v[1] }, false);
});
extensionService.whenInstalledExtensionsRegistered().then(() => {
this.cleanupProviders();
});
this._register(extentionManagementService.onDidUninstallExtension(({ identifier }) => {
let extensionid = getIdFromLocalExtensionId(identifier.id);
extensionService.getExtensions().then(i => {
let extension = i.find(c => c.id === extensionid);
let id = extension.contributes['connectionProvider'].providerId;
delete this.capabilities.connectionProviderCache[id];
});
}));
}
private cleanupProviders(): void {
let knownProviders = Object.keys(connectionRegistry.providers);
for (let key in this.capabilities.connectionProviderCache) {
if (!knownProviders.includes(key)) {
this._providers.delete(key);
delete this.capabilities.connectionProviderCache[key];
}
}
}
private handleConnectionProvider(e: { id: string, properties: ConnectionProviderProperties }, isNew = true): void {
let provider = this._providers.get(e.id);
if (provider) {
provider.connection = e.properties;
} else {
provider = {
connection: e.properties
};
this._providers.set(e.id, provider);
}
if (!this._featureUpdateEvents.has(e.id)) {
this._featureUpdateEvents.set(e.id, new Emitter<ProviderFeatures>());
}
if (isNew) {
this.capabilities.connectionProviderCache[e.id] = e.properties;
this._onCapabilitiesRegistered.fire(provider);
}
}
/**
* Retrieve a list of registered server capabilities
*/
public getCapabilities(provider: string): ProviderFeatures {
return this._providers.get(provider);
}
public getLegacyCapabilities(provider: string): sqlops.DataProtocolServerCapabilities {
return this._legacyProviders.get(provider);
}
public get providers(): { [id: string]: ProviderFeatures } {
return toObject(this._providers);
}
private get capabilities(): CapabilitiesMomento {
return this._momento.getMemento(this._storageService) as CapabilitiesMomento;
}
/**
* Register the capabilities provider and query the provider for its capabilities
* @param provider
*/
public registerProvider(provider: sqlops.CapabilitiesProvider): void {
// request the capabilities from server
provider.getServerCapabilities(clientCapabilities).then(serverCapabilities => {
this._legacyProviders.set(serverCapabilities.providerName, serverCapabilities);
});
}
/**
* Returns true if the feature is available for given connection
* @param featureComponent a component which should have the feature name
* @param connectionManagementInfo connectionManagementInfo
*/
public isFeatureAvailable(action: IAction, connectionManagementInfo: ConnectionManagementInfo): boolean {
let isCloud = connectionManagementInfo && connectionManagementInfo.serverInfo && connectionManagementInfo.serverInfo.isCloud;
let isMssql = connectionManagementInfo.connectionProfile.providerName === 'MSSQL';
// TODO: The logic should from capabilities service.
if (action) {
let featureName: string = action.id;
switch (featureName) {
case Constants.BackupFeatureName:
if (isMssql) {
return connectionManagementInfo.connectionProfile.databaseName && !isCloud;
} else {
return !!connectionManagementInfo.connectionProfile.databaseName;
}
case Constants.RestoreFeatureName:
if (isMssql) {
return !isCloud;
} else {
return !!connectionManagementInfo.connectionProfile.databaseName;
}
default:
return true;
}
} else {
return true;
}
}
public shutdown(): void {
this._momento.saveMemento();
}
}

View File

@@ -0,0 +1,481 @@
/*---------------------------------------------------------------------------------------------
* 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 { ConfigurationEditingService, IConfigurationValue } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ConnectionProfile } from './connectionProfile';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import * as sqlops from 'sqlops';
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 {
/**
* Constructor.
*/
public constructor(
private _configurationEditService: ConfigurationEditingService,
private _workspaceConfigurationService: IWorkspaceConfigurationService,
private _capabilitiesService: ICapabilitiesService
) { }
/**
* 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;
}
/**
* 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.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
if (!profiles) {
profiles = [];
}
let connectionProfile = this.getConnectionProfileInstance(profile, groupId);
let newProfile = ConnectionProfile.convertToProfileStore(this._capabilitiesService, connectionProfile);
// Remove the profile if already set
var sameProfileInList = profiles.find(value => {
let providerConnectionProfile = ConnectionProfile.createFromStoredProfile(value, this._capabilitiesService);
return providerConnectionProfile.matches(connectionProfile);
});
if (sameProfileInList) {
let profileIndex = profiles.findIndex(value => value === sameProfileInList);
newProfile.id = sameProfileInList.id;
connectionProfile.id = sameProfileInList.id;
profiles[profileIndex] = newProfile;
} else {
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;
if (connectionProfile === undefined) {
connectionProfile = new ConnectionProfile(this._capabilitiesService, 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.inspect<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.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let sameNameGroup = groups ? groups.find(group => group.name === profileGroup.name) : undefined;
if (sameNameGroup) {
let errMessage: string = nls.localize('invalidServerName', "A server group with the same name already exists.");
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 => {
return ConnectionProfile.createFromStoredProfile(p, this._capabilitiesService);
});
return connectionProfiles;
}
/**
* Delete a connection profile from settings.
*/
public deleteConnection(profile: ConnectionProfile): Promise<void> {
// Get all connections in the settings
let profiles = this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
// Remove the profile from the connections
profiles = profiles.filter(value => {
let providerConnectionProfile = ConnectionProfile.createFromStoredProfile(value, this._capabilitiesService);
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.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user;
// Remove the profiles from the connections
profiles = profiles.filter(value => {
let providerConnectionProfile = ConnectionProfile.createFromStoredProfile(value, this._capabilitiesService);
return !connections.some((val) => val.getOptionsKey() === providerConnectionProfile.getOptionsKey());
});
// Get all groups in the settings
let groups = this._workspaceConfigurationService.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
// Remove subgroups in the settings
groups = groups.filter((grp) => {
return !subgroups.some((item) => item.id === grp.id);
});
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.inspect<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.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).user :
this._workspaceConfigurationService.inspect<IConnectionProfileStore[]>(Constants.connectionsArrayName).workspace;
if (profiles) {
if (profile.parent && profile.parent.id === Constants.unsavedGroupId) {
profile.groupId = newGroupID;
profiles.push(ConnectionProfile.convertToProfileStore(this._capabilitiesService, profile));
} else {
profiles.forEach((value) => {
let configProf = ConnectionProfile.createFromStoredProfile(value, this._capabilitiesService);
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.inspect<IConnectionProfileGroup[]>(Constants.connectionGroupsArrayName).user;
let sameNameGroup = groups ? groups.find(group => group.name === source.name && group.id !== source.id) : undefined;
if (sameNameGroup) {
let errMessage: string = nls.localize('invalidServerName', "A server group with the same name already exists.");
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): any {
let configs: any;
configs = this._workspaceConfigurationService.inspect<IConnectionProfileStore[] | IConnectionProfileGroup[] | sqlops.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[] | sqlops.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);
}));
});
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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;
}

View File

@@ -0,0 +1,344 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { IConnectionProfileGroup, ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { ConnectionManagementInfo } from './connectionManagementInfo';
import { IServerGroupDialogCallbacks } from 'sql/platform/serverGroup/common/serverGroupController';
export const VIEWLET_ID = 'workbench.view.connections';
/**
* 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;
callStack: string;
errorHandled?: boolean;
connectionProfile?: IConnectionProfile;
}
export interface IConnectionCallbacks {
onConnectStart(): void;
onConnectReject(error?: string): void;
onConnectSuccess(params?: INewConnectionParams): void;
onDisconnect(): void;
onConnectCanceled(): 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<sqlops.DidChangeLanguageFlavorParams>;
/**
* Opens the connection dialog to create new connection
*/
showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): 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', saveConnection?: boolean): 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: sqlops.ConnectionInfoSummary): void;
onIntelliSenseCacheComplete(handle: number, connectionUri: string): void;
onConnectionChangedNotification(handle: number, changedConnInfo: sqlops.ChangedConnectionInfo);
getConnectionGroups(providers?: string[]): ConnectionProfileGroup[];
getRecentConnections(providers?: string[]): ConnectionProfile[];
clearRecentConnectionsList(): void;
clearRecentConnection(connectionProfile: IConnectionProfile): void;
getActiveConnections(providers?: string[]): 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(): sqlops.ConnectionOption[];
getConnectionUri(connectionProfile: IConnectionProfile): string;
getFormattedUri(uri: string, connectionProfile: IConnectionProfile): string;
getConnectionUriFromId(connectionId: string): 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: IConnectionProfile): Promise<void>;
disconnect(ownerUri: string): Promise<void>;
addSavedPassword(connectionProfile: IConnectionProfile): Promise<IConnectionProfile>;
listDatabases(connectionUri: string): Thenable<sqlops.ListDatabasesResult>;
/**
* Register a connection provider
*/
registerProvider(providerId: string, provider: sqlops.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: IConnectionProfile): Thenable<boolean>;
closeDashboard(uri: string): void;
getProviderIdFromUri(ownerUri: string): string;
hasRegisteredServers(): boolean;
canChangeConnectionConfig(profile: IConnectionProfile, newGroupID: string): boolean;
getTabColorForUri(uri: string): string;
/**
* 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;
/**
* Refresh the IntelliSense cache for the connection with the given URI
*/
rebuildIntelliSenseCache(uri: string): Thenable<void>;
/**
* Get a copy of the connection profile with its passwords removed
* @param {IConnectionProfile} profile The connection profile to remove passwords from
* @returns {IConnectionProfile} A copy of the connection profile with passwords removed
*/
removeConnectionProfileCredentials(profile: IConnectionProfile): IConnectionProfile;
/**
* Get the credentials for a connected connection profile, as they would appear in the options dictionary
* @param {string} profileId The id of the connection profile to get the password for
* @returns {{ [name: string]: string }} A dictionary containing the credentials as they would be included
* in the connection profile's options dictionary, or undefined if the profile is not connected
*/
getActiveConnectionCredentials(profileId: string): { [name: string]: string };
/**
* Get the connection string for the provided connection ID
*/
getConnectionString(connectionId: string, includePassword: boolean): Thenable<string>;
/**
* Serialize connection string with optional provider
*/
buildConnectionInfo(connectionString: string, provider?: string): Thenable<sqlops.ConnectionInfo>;
}
export const IConnectionDialogService = createDecorator<IConnectionDialogService>('connectionDialogService');
export interface IConnectionDialogService {
_serviceBrand: any;
/**
* Opens the connection dialog and returns the promise for successfully opening the dialog
* @param connectionManagementService
* @param params
* @param model
* @param connectionResult
*/
showDialog(connectionManagementService: IConnectionManagementService, params: INewConnectionParams, model: IConnectionProfile, connectionResult?: IConnectionResult): Thenable<void>;
/**
* Opens the connection dialog and returns the promise when connection is made
* or dialog is closed
* @param connectionManagementService
* @param params
* @param model
* @param connectionResult
*/
openDialogAndWait(connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Thenable<IConnectionProfile>;
}
export enum RunQueryOnConnectionMode {
none = 0,
executeQuery = 1,
executeCurrentQuery = 2,
estimatedQueryPlan = 3,
actualQueryPlan = 4
}
export interface INewConnectionParams {
connectionType: ConnectionType;
input?: IConnectableInput;
runQueryOnCompletion?: RunQueryOnConnectionMode;
querySelection?: sqlops.ISelectionData;
showDashboard?: boolean;
providers?: string[];
}
export interface IConnectableInput {
uri: string;
onConnectStart(): void;
onConnectReject(error?: string): void;
onConnectSuccess(params?: INewConnectionParams): void;
onDisconnect(): void;
onConnectCanceled(): 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,
Canceling = 6
}
export interface IConnectionParams {
connectionUri: string;
connectionProfile: IConnectionProfile;
}

View 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/platform/connection/common/connectionProfile';
import * as sqlops from 'sqlops';
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, callStack?: string) => 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: sqlops.ServerInfo;
/**
* Owner uri assigned to the connection
*/
public ownerUri: string;
}

File diff suppressed because it is too large Load Diff

View 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 { ConnectionProfileGroup } from './connectionProfileGroup';
import * as sqlops from 'sqlops';
import { ProviderConnectionInfo } from 'sql/platform/connection/common/providerConnectionInfo';
import * as interfaces from 'sql/platform/connection/common/interfaces';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import { generateUuid } from 'vs/base/common/uuid';
import * as objects from 'sql/base/common/objects';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { isString } from 'vs/base/common/types';
// 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(
capabilitiesService: ICapabilitiesService,
model: string | sqlops.IConnectionProfile
) {
super(capabilitiesService, model);
if (model && !isString(model)) {
this.groupId = model.groupId;
this.groupFullName = model.groupFullName;
this.savePassword = model.savePassword;
this.saveProfile = model.saveProfile;
this._id = model.id;
this.azureTenantId = model.azureTenantId;
} 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 azureTenantId(): string {
return this.options['azureTenantId'];
}
public set azureTenantId(value: string) {
this.options['azureTenantId'] = 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.capabilitiesService, 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 toIConnectionProfile(): interfaces.IConnectionProfile {
let result: interfaces.IConnectionProfile = {
connectionName: this.connectionName,
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,
azureTenantId: this.azureTenantId
};
return result;
}
public toConnectionInfo(): sqlops.ConnectionInfo {
return {
options: this.options
};
}
public static fromIConnectionProfile(capabilitiesService: ICapabilitiesService, profile: sqlops.IConnectionProfile) {
if (profile) {
if (profile instanceof ConnectionProfile) {
return profile;
} else {
return new ConnectionProfile(capabilitiesService, profile);
}
}
return undefined;
}
public static createFromStoredProfile(profile: interfaces.IConnectionProfileStore, capabilitiesService: ICapabilitiesService): ConnectionProfile {
let connectionInfo = new ConnectionProfile(capabilitiesService, profile.providerName);
connectionInfo.options = profile.options;
// append group ID and original display name to build unique OE session ID
connectionInfo.options = objects.clone(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 convertToProfileStore(
capabilitiesService: ICapabilitiesService,
connectionProfile: interfaces.IConnectionProfile): interfaces.IConnectionProfileStore {
if (connectionProfile) {
let connectionInfo = ConnectionProfile.fromIConnectionProfile(capabilitiesService, 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;
}
}
}

View File

@@ -0,0 +1,219 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
/**
* Returns true if all connections in the tree have valid options using the correct capabilities
*/
public get hasValidConnections(): boolean {
if (this.connections) {
let invalidConnections = this.connections.find(c => !c.isConnectionOptionsValid);
if (invalidConnections !== undefined) {
return false;
} else {
let childrenAreValid: boolean = true;
this.children.forEach(element => {
let isChildValid = element.hasValidConnections;
if (!isChildValid) {
childrenAreValid = false;
}
});
return childrenAreValid;
}
} else {
return true;
}
}
public getChildren(): any {
let allChildren = [];
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 isAncestorOf(node: ConnectionProfileGroup | ConnectionProfile): boolean {
let isAncestor = false;
let currentNode = node;
while (currentNode) {
if (currentNode.parent && currentNode.parent.id === this.id) {
isAncestor = true;
break;
}
currentNode = currentNode.parent;
}
return isAncestor;
}
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;
}
}

View File

@@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* 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/platform/capabilities/common/capabilitiesService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionProfile } from './interfaces';
import * as Utils from './utils';
import * as sqlops from 'sqlops';
import { StopWatch } from 'vs/base/common/stopwatch';
export class ConnectionStatusManager {
private _connections: { [id: string]: ConnectionManagementInfo };
private _providerCapabilitiesMap: { [providerName: string]: sqlops.DataProtocolServerCapabilities };
constructor( @ICapabilitiesService private _capabilitiesService: ICapabilitiesService) {
this._connections = {};
this._providerCapabilitiesMap = {};
}
public findConnection(uri: string): ConnectionManagementInfo {
if (uri in this._connections) {
return this._connections[uri];
} else {
return undefined;
}
}
public findConnectionByProfileId(profileId: string): ConnectionManagementInfo {
return Object.values(this._connections).find((connection: ConnectionManagementInfo) => connection.connectionProfile.id === profileId);
}
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._capabilitiesService, connection);
let connectionInfo: ConnectionManagementInfo = new ConnectionManagementInfo();
connectionInfo.providerId = connection.providerName;
connectionInfo.extensionTimer = StopWatch.create();
connectionInfo.intelliSenseTimer = StopWatch.create();
connectionInfo.connectionProfile = connectionProfile;
connectionInfo.connecting = true;
this._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: sqlops.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: sqlops.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 the given ownerUri
* The purpose for this method is to find the connection given the ownerUri and find the original uri assigned to it. most of the times should be the same.
* Only if the db name in the original uri is different 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: sqlops.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;
}
/**
* Get a list of the active connection profiles managed by the status manager
*/
public getActiveConnectionProfiles(providers?: string[]): ConnectionProfile[] {
let profiles = Object.values(this._connections).map((connectionInfo: ConnectionManagementInfo) => connectionInfo.connectionProfile);
// Remove duplicate profiles that may be listed multiple times under different URIs by filtering for profiles that don't have the same ID as an earlier profile in the list
profiles = profiles.filter((profile, index) => profiles.findIndex(otherProfile => otherProfile.id === profile.id) === index);
if (providers) {
profiles = profiles.filter(f => providers.includes(f.providerName));
}
return profiles;
}
}

View File

@@ -0,0 +1,564 @@
/*---------------------------------------------------------------------------------------------
* 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/platform/connection/common/interfaces';
import { ICredentialsService } from 'sql/platform/credentials/common/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 { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
import * as sqlops from 'sqlops';
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: ConfigurationEditingService,
private _workspaceConfigurationService: IWorkspaceConfigurationService,
private _credentialService: ICredentialsService,
private _capabilitiesService: ICapabilitiesService,
private _connectionConfig?: IConnectionConfig
) {
if (_context) {
this._memento = this._context.getMemento(this._storageService, MementoScope.GLOBAL);
}
this._groupIdToFullNameMap = {};
this._groupFullNameToIdMap = {};
if (!this._connectionConfig) {
this._connectionConfig = new ConnectionConfig(this._configurationEditService,
this._workspaceConfigurationService, this._capabilitiesService);
}
}
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.fromIConnectionProfile(
this._capabilitiesService, 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.fromIConnectionProfile(this._capabilitiesService, connection);
return connectionProfile.isPasswordRequired();
} else {
return false;
}
}
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;
credentialsItem.options['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);
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);
}
});
}
/**
* 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 {sqlops.ConnectionInfo} the array of connections, empty if none are found
*/
public getRecentlyUsedConnections(providers?: string[]): ConnectionProfile[] {
let configValues: IConnectionProfile[] = this._memento[Constants.recentConnections];
if (!configValues) {
configValues = [];
}
configValues = configValues.filter(c => !!(c));
if (providers && providers.length > 0) {
configValues = configValues.filter(c => providers.includes(c.providerName));
}
return this.convertConfigValuesToConnectionProfiles(configValues);
}
private convertConfigValuesToConnectionProfiles(configValues: IConnectionProfile[]): ConnectionProfile[] {
return configValues.map(c => {
if (c) {
let connectionProfile = new ConnectionProfile(this._capabilitiesService, c);
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 {sqlops.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.fromIConnectionProfile(this._capabilitiesService, 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, isConnectionToDefaultDb: boolean = false): Promise<void> {
if (this.getActiveConnections().some(existingConn => existingConn.id === conn.id)) {
return Promise.resolve(undefined);
} else {
return this.addConnectionToMemento(conn, Constants.activeConnections, undefined, conn.savePassword).then(() => {
let maxConnections = this.getMaxRecentConnectionsCount();
if (isConnectionToDefaultDb) {
conn.databaseName = '';
}
return this.addConnectionToMemento(conn, Constants.recentConnections, maxConnections);
});
}
}
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 = this.addToConnectionList(conn, configValues);
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);
}
});
}
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[]): 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;
});
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, providers?: string[]): ConnectionProfileGroup[] {
let profilesInConfiguration: ConnectionProfile[];
if (!withoutConnections) {
profilesInConfiguration = this._connectionConfig.getConnections(true);
if (providers && providers.length > 0) {
profilesInConfiguration = profilesInConfiguration.filter(x => providers.includes(x.providerName));
}
}
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;
}
public getGroupFromId(groupId: string): IConnectionProfileGroup {
let groups = this._connectionConfig.getAllGroups();
return groups.find(group => group.id === groupId);
}
private getMaxRecentConnectionsCount(): number {
let config = this._workspaceConfigurationService.getValue(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;
}
}

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// 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 anyProviderName = '*';
export const connectionProviderContextKey = 'connectionProvider';
export const applicationName = 'sqlops';
export const defaultEngine = 'defaultEngine';
export const passwordChars = '***************';
/* authentication types */
export const sqlLogin = 'SqlLogin';
export const integrated = 'Integrated';
export const azureMFA = 'AzureMFA';

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IConnectionProfile } from './interfaces';
import { IConnectionProfileGroup, ConnectionProfileGroup } from './connectionProfileGroup';
import { ConnectionProfile } from './connectionProfile';
import * as sqlops from 'sqlops';
/**
* 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>;
editGroup(group: ConnectionProfileGroup): Promise<void>;
deleteConnection(profile: ConnectionProfile): Promise<void>;
deleteGroup(group: ConnectionProfileGroup): Promise<void>;
canChangeConnectionConfig(profile: ConnectionProfile, newGroupID: string): boolean;
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
export interface IConnectionProfile extends sqlops.IConnectionProfile {
getOptionsKey(): string;
matches(profile: sqlops.IConnectionProfile): boolean;
}
export interface IConnectionProfileStore {
options: {};
groupId: string;
providerName: string;
savePassword: boolean;
id: string;
}

View File

@@ -0,0 +1,305 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import { isString } from 'vs/base/common/types';
import * as sqlops from 'sqlops';
import { ConnectionOptionSpecialType, ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
import * as Constants from 'sql/platform/connection/common/constants';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ConnectionProviderProperties } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
export class ProviderConnectionInfo extends Disposable implements sqlops.ConnectionInfo {
options: { [name: string]: any } = {};
private _providerName: string;
protected _serverCapabilities: ConnectionProviderProperties;
private static readonly SqlAuthentication = 'SqlLogin';
public static readonly ProviderPropertyName = 'providerName';
public constructor(
protected capabilitiesService: ICapabilitiesService,
model: string | sqlops.IConnectionProfile
) {
super();
// we can't really do a whole lot if we don't have a provider
if (isString(model) || (model && model.providerName)) {
this.providerName = isString(model) ? model : model.providerName;
if (!isString(model)) {
if (model.options && this._serverCapabilities) {
this._serverCapabilities.connectionOptions.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;
this.connectionName = model.connectionName;
}
}
}
public get providerName(): string {
return this._providerName;
}
public set providerName(name: string) {
this._providerName = name;
if (!this._serverCapabilities) {
let capabilities = this.capabilitiesService.getCapabilities(this.providerName);
if (capabilities) {
this._serverCapabilities = capabilities.connection;
}
this._register(this.capabilitiesService.onCapabilitiesRegistered(e => {
if (e.connection.providerId === this.providerName) {
this._serverCapabilities = e.connection;
}
}));
}
}
public clone(): ProviderConnectionInfo {
let instance = new ProviderConnectionInfo(this.capabilitiesService, this.providerName);
instance.options = Object.assign({}, this.options);
return instance;
}
public get serverCapabilities(): ConnectionProviderProperties {
return this._serverCapabilities;
}
public get connectionName(): string {
return this.getSpecialTypeOptionValue(ConnectionOptionSpecialType.connectionName);
}
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 connectionName(value: string) {
this.setSpecialTypeOptionName(ConnectionOptionSpecialType.connectionName, value);
}
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;
}
private getServerInfo() {
let databaseName = this.databaseName ? this.databaseName : '<default>';
let userName = this.userName ? this.userName : 'Windows Authentication';
return this.serverName + ', ' + databaseName + ' (' + userName + ')';
}
/**
* Returns the title of the connection
*/
public get title(): string {
let label = '';
if (this.connectionName) {
label = this.connectionName;
} else {
label = this.getServerInfo();
}
return label;
}
public get serverInfo(): string {
return this.getServerInfo();
}
/**
* Returns true if the capabilities and options are loaded correctly
*/
public get isConnectionOptionsValid(): boolean {
return this.serverCapabilities && this.title.indexOf('undefined') < 0;
}
public isPasswordRequired(): boolean {
let optionMetadata = this._serverCapabilities.connectionOptions.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: string): 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.connectionOptions.map(o => {
if ((o.specialValueType || o.isIdentity)
&& o.specialValueType !== ConnectionOptionSpecialType.password
&& o.specialValueType !== ConnectionOptionSpecialType.connectionName) {
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: string): string {
if (this._serverCapabilities) {
let optionMetadata = this._serverCapabilities.connectionOptions.find(o => o.specialValueType === type);
return !!optionMetadata ? optionMetadata.name : undefined;
} else {
return type.toString();
}
}
public setSpecialTypeOptionName(type: string, value: string): void {
let name = this.getSpecialTypeOptionName(type);
if (!!name) {
this.options[name] = value;
}
}
public get authenticationTypeDisplayName(): string {
let optionMetadata = this._serverCapabilities.connectionOptions.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(): sqlops.ConnectionOption[] {
return this._serverCapabilities.connectionOptions;
}
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.connectionOptions.forEach(element => {
if (element.specialValueType !== ConnectionOptionSpecialType.serverName &&
element.specialValueType !== ConnectionOptionSpecialType.databaseName &&
element.specialValueType !== ConnectionOptionSpecialType.authType &&
element.specialValueType !== ConnectionOptionSpecialType.password &&
element.specialValueType !== ConnectionOptionSpecialType.connectionName &&
element.isIdentity && element.valueType === ServiceOptionType.string) {
let value = this.getOptionValue(element.name);
if (value) {
parts.push(value);
}
}
});
return parts;
}
}

View File

@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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, includeFraction: boolean = true): 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 && includeFraction ? rs + '.' + mss : rs;
}
export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection' | 'notebook'): 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';
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* 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 { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import * as sqlops from 'sqlops';
import { Deferred } from 'sql/base/common/promise';
export const SERVICE_ID = 'credentialsService';
export interface CredentialManagementEvents {
onSaveCredential(credentialId: string, password: string): Thenable<boolean>;
onReadCredential(credentialId: string): Thenable<sqlops.Credential>;
onDeleteCredential(credentialId: string): Thenable<boolean>;
}
export const ICredentialsService = createDecorator<ICredentialsService>(SERVICE_ID);
export interface ICredentialsService {
_serviceBrand: any;
saveCredential(credentialId: string, password: string): Thenable<boolean>;
readCredential(credentialId: string): Thenable<sqlops.Credential>;
deleteCredential(credentialId: string): Thenable<boolean>;
addEventListener(handle: number, events: CredentialManagementEvents): IDisposable;
}
export class CredentialsService implements ICredentialsService {
_serviceBrand: any;
private disposables: IDisposable[] = [];
private _serverEvents: { [handle: number]: CredentialManagementEvents; } = Object.create(null);
private _lastHandle: number;
private _onServerEventsReady: Deferred<void> = new Deferred<void>();
constructor() {
}
public addEventListener(handle: number, events: CredentialManagementEvents): IDisposable {
this._lastHandle = handle;
this._serverEvents[handle] = events;
this._onServerEventsReady.resolve();
return {
dispose: () => {
}
};
}
public saveCredential(credentialId: string, password: string): Thenable<boolean> {
return this._onServerEventsReady.promise.then(() => this._serverEvents[this._lastHandle].onSaveCredential(credentialId, password));
}
public readCredential(credentialId: string): Thenable<sqlops.Credential> {
return this._onServerEventsReady.promise.then(() => this._serverEvents[this._lastHandle].onReadCredential(credentialId));
}
public deleteCredential(credentialId: string): Thenable<boolean> {
return this._onServerEventsReady.promise.then(() => this._serverEvents[this._lastHandle].onDeleteCredential(credentialId));
}
public dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@@ -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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { TPromise } from 'vs/base/common/winjs.base';
import { localize } from 'vs/nls';
export const SERVICE_ID = 'dacFxService';
export const IDacFxService = createDecorator<IDacFxService>(SERVICE_ID);
export interface IDacFxService {
_serviceBrand: any;
registerProvider(providerId: string, provider: sqlops.DacFxServicesProvider): void;
exportBacpac(sourceDatabaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void;
importBacpac(packageFilePath: string, targetDatabaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void;
extractDacpac(sourceDatabaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void;
deployDacpac(packageFilePath: string, targetDatabaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void;
}
export class DacFxService implements IDacFxService {
_serviceBrand: any;
private _providers: { [handle: string]: sqlops.DacFxServicesProvider; } = Object.create(null);
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService
) {
}
registerProvider(providerId: string, provider: sqlops.DacFxServicesProvider): void {
this._providers[providerId] = provider;
}
exportBacpac(databasesName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return this._runAction(ownerUri, (runner) => {
return runner.exportBacpac(databasesName, packageFilePath, ownerUri, taskExecutionMode);
});
}
importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return this._runAction(ownerUri, (runner) => {
return runner.importBacpac(packageFilePath, databaseName, ownerUri, taskExecutionMode);
});
}
extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return this._runAction(ownerUri, (runner) => {
return runner.extractDacpac(databaseName, packageFilePath, applicationName, applicationVersion, ownerUri, taskExecutionMode);
});
}
deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return this._runAction(ownerUri, (runner) => {
return runner.deployDacpac(packageFilePath, databaseName, upgradeExisting, ownerUri, taskExecutionMode);
});
}
private _runAction<T>(uri: string, action: (handler: sqlops.DacFxServicesProvider) => 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 DacFxService")));
}
let handler = this._providers[providerId];
if (handler) {
return action(handler);
} else {
return TPromise.wrapError(new Error(localize('noHandlerRegistered', "No Handler Registered")));
}
}
}

View File

@@ -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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import * as DOM from 'vs/base/browser/dom';
import * as sqlops from 'sqlops';
export const IDashboardService = createDecorator<IDashboardService>('dashboardService');
export interface IDashboardService {
_serviceBrand: any;
readonly onDidOpenDashboard: Event<sqlops.DashboardDocument>;
readonly onDidChangeToDashboard: Event<sqlops.DashboardDocument>;
readonly onLayout: Event<DOM.Dimension>;
openDashboard(document: sqlops.DashboardDocument): void;
changeToDashboard(document: sqlops.DashboardDocument): void;
layout(dimension: DOM.Dimension): void;
}

View 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 { IDashboardService } from './dashboardService';
import { Event, Emitter } from 'vs/base/common/event';
import * as DOM from 'vs/base/browser/dom';
import * as sqlops from 'sqlops';
export class DashboardService implements IDashboardService {
public _serviceBrand: any;
private _onDidOpenDashboard = new Emitter<sqlops.DashboardDocument>();
public readonly onDidOpenDashboard: Event<sqlops.DashboardDocument> = this._onDidOpenDashboard.event;
private _onDidChangeToDashboard = new Emitter<sqlops.DashboardDocument>();
public readonly onDidChangeToDashboard: Event<sqlops.DashboardDocument> = this._onDidChangeToDashboard.event;
private _onLayout = new Emitter<DOM.Dimension>();
public readonly onLayout: Event<DOM.Dimension> = this._onLayout.event;
public openDashboard(document: sqlops.DashboardDocument): void {
this._onDidOpenDashboard.fire(document);
}
public changeToDashboard(document: sqlops.DashboardDocument): void {
this._onDidChangeToDashboard.fire(document);
}
public layout(dimension: DOM.Dimension): void {
this._onLayout.fire(dimension);
}
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IView, IModelView } from 'sql/platform/model/common/modelViewService';
export const SERVICE_ID = 'dashboardViewService';
export interface IDashboardWebview extends IView {
setHtml(html: string): void;
onMessage: Event<string>;
sendMessage(message: string);
}
export interface IDashboardViewService {
_serviceBrand: any;
onRegisteredWebview: Event<IDashboardWebview>;
registerWebview(widget: IDashboardWebview);
onRegisteredModelView: Event<IModelView>;
registerModelView(widget: IModelView);
}
export const IDashboardViewService = createDecorator<IDashboardViewService>(SERVICE_ID);

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* 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 { IDashboardViewService, IDashboardWebview } from 'sql/platform/dashboard/common/dashboardViewService';
import { Event, Emitter } from 'vs/base/common/event';
import { IModelView } from 'sql/platform/model/common/modelViewService';
export class DashboardViewService implements IDashboardViewService {
_serviceBrand: any;
private _onRegisteredWebview = new Emitter<IDashboardWebview>();
public readonly onRegisteredWebview: Event<IDashboardWebview> = this._onRegisteredWebview.event;
private _onRegisteredModelView = new Emitter<IModelView>();
public readonly onRegisteredModelView: Event<IModelView> = this._onRegisteredModelView.event;
public registerWebview(widget: IDashboardWebview) {
this._onRegisteredWebview.fire(widget);
}
registerModelView(view: IModelView) {
this._onRegisteredModelView.fire(view);
}
}

View 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 Severity from 'vs/base/common/severity';
import { IAction } from 'vs/base/common/actions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IErrorMessageService = createDecorator<IErrorMessageService>('errorMessageService');
export interface IErrorMessageService {
_serviceBrand: any;
showDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string, actions?: IAction[]): void;
}

View File

@@ -0,0 +1,233 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { FileBrowserTree } from 'sql/parts/fileBrowser/common/fileBrowserTree';
import { FileNode } from 'sql/parts/fileBrowser/common/fileNode';
import { IFileBrowserService } from 'sql/platform/fileBrowser/common/interfaces';
import * as Constants from 'sql/common/constants';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { Event, Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import * as strings from 'vs/base/common/strings';
export class FileBrowserService implements IFileBrowserService {
public _serviceBrand: any;
private _providers: { [handle: string]: sqlops.FileBrowserProvider; } = Object.create(null);
private _onAddFileTree = new Emitter<FileBrowserTree>();
private _onExpandFolder = new Emitter<FileNode>();
private _onPathValidate = new Emitter<sqlops.FileBrowserValidatedParams>();
private _pathToFileNodeMap: { [path: string]: FileNode } = {};
private _expandResolveMap: { [key: string]: any } = {};
static fileNodeId: number = 0;
constructor( @IConnectionManagementService private _connectionService: IConnectionManagementService,
@IErrorMessageService private _errorMessageService: IErrorMessageService) {
}
public registerProvider(providerId: string, provider: sqlops.FileBrowserProvider): void {
this._providers[providerId] = provider;
}
public get onAddFileTree(): Event<FileBrowserTree> {
return this._onAddFileTree.event;
}
public get onExpandFolder(): Event<FileNode> {
return this._onExpandFolder.event;
}
public get onPathValidate(): Event<sqlops.FileBrowserValidatedParams> {
return this._onPathValidate.event;
}
public openFileBrowser(ownerUri: string, expandPath: string, fileFilters: string[], changeFilter: boolean): Thenable<boolean> {
return new Promise<boolean>((resolve, reject) => {
let provider = this.getProvider(ownerUri);
if (provider) {
provider.openFileBrowser(ownerUri, expandPath, fileFilters, changeFilter).then(result => {
resolve(result);
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
public onFileBrowserOpened(handle: number, fileBrowserOpenedParams: sqlops.FileBrowserOpenedParams) {
if (fileBrowserOpenedParams.succeeded === true
&& fileBrowserOpenedParams.fileTree
&& fileBrowserOpenedParams.fileTree.rootNode
&& fileBrowserOpenedParams.fileTree.selectedNode
) {
var fileTree = this.convertFileTree(null, fileBrowserOpenedParams.fileTree.rootNode, fileBrowserOpenedParams.fileTree.selectedNode.fullPath, fileBrowserOpenedParams.ownerUri);
this._onAddFileTree.fire({ rootNode: fileTree.rootNode, selectedNode: fileTree.selectedNode, expandedNodes: fileTree.expandedNodes });
} else {
let genericErrorMessage = localize('fileBrowserErrorMessage', 'An error occured while loading the file browser.');
let errorDialogTitle = localize('fileBrowserErrorDialogTitle', 'File browser error');
let errorMessage = strings.isFalsyOrWhitespace(fileBrowserOpenedParams.message) ? genericErrorMessage : fileBrowserOpenedParams.message;
this._errorMessageService.showDialog(Severity.Error, errorDialogTitle, errorMessage);
}
}
public expandFolderNode(fileNode: FileNode): Thenable<FileNode[]> {
this._pathToFileNodeMap[fileNode.fullPath] = fileNode;
let self = this;
return new Promise<FileNode[]>((resolve, reject) => {
let provider = this.getProvider(fileNode.ownerUri);
if (provider) {
provider.expandFolderNode(fileNode.ownerUri, fileNode.fullPath).then(result => {
var mapKey = self.generateResolveMapKey(fileNode.ownerUri, fileNode.fullPath);
self._expandResolveMap[mapKey] = resolve;
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
public onFolderNodeExpanded(handle: number, fileBrowserExpandedParams: sqlops.FileBrowserExpandedParams) {
var mapKey = this.generateResolveMapKey(fileBrowserExpandedParams.ownerUri, fileBrowserExpandedParams.expandPath);
var expandResolve = this._expandResolveMap[mapKey];
if (expandResolve) {
if (fileBrowserExpandedParams.succeeded === true) {
// get the expanded folder node
var expandedNode = this._pathToFileNodeMap[fileBrowserExpandedParams.expandPath];
if (expandedNode) {
if (fileBrowserExpandedParams.children && fileBrowserExpandedParams.children.length > 0) {
expandedNode.children = this.convertChildren(expandedNode, fileBrowserExpandedParams.children, fileBrowserExpandedParams.ownerUri);
}
expandResolve(expandedNode.children ? expandedNode.children : []);
this._onExpandFolder.fire(expandedNode);
} else {
expandResolve([]);
}
} else {
expandResolve([]);
}
}
}
public validateFilePaths(ownerUri: string, serviceType: string, selectedFiles: string[]): Thenable<boolean> {
return new Promise<boolean>((resolve, reject) => {
let provider = this.getProvider(ownerUri);
if (provider) {
provider.validateFilePaths(ownerUri, serviceType, selectedFiles).then(result => {
resolve(result);
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
public onFilePathsValidated(handle: number, fileBrowserValidatedParams: sqlops.FileBrowserValidatedParams) {
this._onPathValidate.fire(fileBrowserValidatedParams);
}
public closeFileBrowser(ownerUri: string): Thenable<sqlops.FileBrowserCloseResponse> {
let provider = this.getProvider(ownerUri);
if (provider) {
return provider.closeFileBrowser(ownerUri);
}
return Promise.resolve(undefined);
}
private generateResolveMapKey(ownerUri: string, expandPath: string): string {
return ownerUri + ':' + expandPath;
}
private getProvider(connectionUri: string): sqlops.FileBrowserProvider {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
return this._providers[providerId];
} else {
return undefined;
}
}
private convertFileTree(parentNode: FileNode, fileTreeNode: sqlops.FileTreeNode, expandPath: string, ownerUri: string): FileBrowserTree {
FileBrowserService.fileNodeId += 1;
var expandedNodes: FileNode[] = [];
var selectedNode: FileNode;
var fileNode = new FileNode(FileBrowserService.fileNodeId.toString(),
fileTreeNode.name,
fileTreeNode.fullPath,
fileTreeNode.isFile,
fileTreeNode.isExpanded,
ownerUri,
parentNode
);
if (fileNode.isExpanded === true) {
expandedNodes.push(fileNode);
}
if (fileTreeNode.children) {
var convertedChildren = [];
for (var i = 0; i < fileTreeNode.children.length; i++) {
var convertedFileTree: FileBrowserTree = this.convertFileTree(fileNode, fileTreeNode.children[i], expandPath, ownerUri);
convertedChildren.push(convertedFileTree.rootNode);
if (convertedFileTree.expandedNodes.length > 0) {
expandedNodes = expandedNodes.concat(convertedFileTree.expandedNodes);
}
if (convertedFileTree.selectedNode) {
selectedNode = convertedFileTree.selectedNode;
}
}
if (convertedChildren.length > 0) {
fileNode.children = convertedChildren;
}
}
if (!selectedNode && fileTreeNode.fullPath === expandPath) {
selectedNode = fileNode;
}
// Assume every folder has children initially
if (fileTreeNode.isFile === false) {
fileNode.hasChildren = true;
}
return { rootNode: fileNode, selectedNode: selectedNode, expandedNodes: expandedNodes };
}
private convertChildren(expandedNode: FileNode, childrenToConvert: sqlops.FileTreeNode[], ownerUri: string): FileNode[] {
var childrenNodes = [];
for (var i = 0; i < childrenToConvert.length; i++) {
FileBrowserService.fileNodeId += 1;
var childNode = new FileNode(FileBrowserService.fileNodeId.toString(),
childrenToConvert[i].name,
childrenToConvert[i].fullPath,
childrenToConvert[i].isFile,
childrenToConvert[i].isExpanded,
ownerUri,
expandedNode
);
// Assume every folder has children initially
if (childrenToConvert[i].isFile === false) {
childNode.hasChildren = true;
}
childrenNodes.push(childNode);
}
return childrenNodes;
}
}

View File

@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import { FileBrowserTree } from 'sql/parts/fileBrowser/common/fileBrowserTree';
import { FileNode } from 'sql/parts/fileBrowser/common/fileNode';
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IFileBrowserDialogController = createDecorator<IFileBrowserDialogController>('fileBrowserDialogService');
export interface IFileBrowserDialogController {
_serviceBrand: any;
/**
* Show file browser dialog
*/
showDialog(ownerUri: string,
expandPath: string,
fileFilters: { label: string, filters: string[] }[],
fileValidationServiceType: string,
isWide: boolean,
handleOnOk: (path: string) => void): void;
}
export const IFileBrowserService = createDecorator<IFileBrowserService>('fileBrowserService');
export interface IFileBrowserService {
_serviceBrand: any;
onAddFileTree: Event<FileBrowserTree>;
onExpandFolder: Event<FileNode>;
onPathValidate: Event<sqlops.FileBrowserValidatedParams>;
/**
* Register file browser provider
*/
registerProvider(providerId: string, provider: sqlops.FileBrowserProvider): void;
/**
* Open file browser
*/
openFileBrowser(ownerUri: string, expandPath: string, fileFilters: string[], changeFilter: boolean): Thenable<boolean>;
/**
* Event called when file browser is opened
*/
onFileBrowserOpened(handle: number, fileBrowserOpenedParams: sqlops.FileBrowserOpenedParams);
/**
* Expand folder node
*/
expandFolderNode(fileNode: FileNode): Thenable<FileNode[]>;
/**
* Event called when children nodes are retrieved
*/
onFolderNodeExpanded(handle: number, fileBrowserExpandedParams: sqlops.FileBrowserExpandedParams);
/**
* Validate selected file paths
*/
validateFilePaths(ownerUri: string, serviceType: string, selectedFiles: string[]): Thenable<boolean>;
/**
* Event called when the validation is complete
*/
onFilePathsValidated(handle: number, fileBrowserValidatedParams: sqlops.FileBrowserValidatedParams);
/**
* Close file browser
*/
closeFileBrowser(ownerUri: string): Thenable<sqlops.FileBrowserCloseResponse>;
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { JobCacheObject, AlertsCacheObject, ProxiesCacheObject, OperatorsCacheObject } from './jobManagementService';
import { Event } from 'vs/base/common/event';
export const SERVICE_ID = 'jobManagementService';
export const IJobManagementService = createDecorator<IJobManagementService>(SERVICE_ID);
export interface IJobManagementService {
_serviceBrand: any;
onDidChange: Event<void>;
registerProvider(providerId: string, provider: sqlops.AgentServicesProvider): void;
fireOnDidChange(): void;
getJobs(connectionUri: string): Thenable<sqlops.AgentJobsResult>;
getJobHistory(connectionUri: string, jobID: string, jobName: string): Thenable<sqlops.AgentJobHistoryResult>;
deleteJob(connectionUri: string, job: sqlops.AgentJobInfo): Thenable<sqlops.ResultStatus>;
deleteJobStep(connectionUri: string, step: sqlops.AgentJobStepInfo): Thenable<sqlops.ResultStatus>;
getAlerts(connectionUri: string): Thenable<sqlops.AgentAlertsResult>;
deleteAlert(connectionUri: string, alert: sqlops.AgentAlertInfo): Thenable<sqlops.ResultStatus>;
getOperators(connectionUri: string): Thenable<sqlops.AgentOperatorsResult>;
deleteOperator(connectionUri: string, operator: sqlops.AgentOperatorInfo): Thenable<sqlops.ResultStatus>;
getProxies(connectionUri: string): Thenable<sqlops.AgentProxiesResult>;
deleteProxy(connectionUri: string, proxy: sqlops.AgentProxyInfo): Thenable<sqlops.ResultStatus>;
getCredentials(connectionUri: string): Thenable<sqlops.GetCredentialsResult>;
jobAction(connectionUri: string, jobName: string, action: string): Thenable<sqlops.ResultStatus>;
addToCache(server: string, cache: JobCacheObject | OperatorsCacheObject);
jobCacheObjectMap: { [server: string]: JobCacheObject; };
operatorsCacheObjectMap: { [server: string]: OperatorsCacheObject; };
alertsCacheObjectMap: { [server: string]: AlertsCacheObject; };
proxiesCacheObjectMap: { [server: string]: ProxiesCacheObject };
addToCache(server: string, cache: JobCacheObject | ProxiesCacheObject | AlertsCacheObject | OperatorsCacheObject);
}

View File

@@ -0,0 +1,539 @@
/*---------------------------------------------------------------------------------------------
* 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 { Action } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
import * as sqlops from 'sqlops';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component';
import { IJobManagementService } from '../common/interfaces';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { JobsViewComponent } from 'sql/parts/jobManagement/views/jobsView.component';
import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.component';
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
export const successLabel: string = nls.localize('jobaction.successLabel', 'Success');
export const errorLabel: string = nls.localize('jobaction.faillabel', 'Error');
export enum JobActions {
Run = 'run',
Stop = 'stop'
}
export class IJobActionInfo {
ownerUri: string;
targetObject: any;
jobHistoryComponent?: JobHistoryComponent;
jobViewComponent?: JobsViewComponent;
}
// Job actions
export class JobsRefreshAction extends Action {
public static ID = 'jobaction.refresh';
public static LABEL = nls.localize('jobaction.refresh', "Refresh");
constructor(
) {
super(JobsRefreshAction.ID, JobsRefreshAction.LABEL, 'refreshIcon');
}
public run(context: IJobActionInfo): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
if (context) {
context.jobHistoryComponent.refreshJobs();
resolve(true);
} else {
reject(false);
}
});
}
}
export class NewJobAction extends Action {
public static ID = 'jobaction.newJob';
public static LABEL = nls.localize('jobaction.newJob', "New Job");
constructor(
) {
super(NewJobAction.ID, NewJobAction.LABEL, 'newStepIcon');
}
public run(context: JobsViewComponent): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
context.openCreateJobDialog();
resolve(true);
} catch (e) {
reject(e);
}
});
}
}
export class RunJobAction extends Action {
public static ID = 'jobaction.runJob';
public static LABEL = nls.localize('jobaction.run', "Run");
constructor(
@INotificationService private notificationService: INotificationService,
@IErrorMessageService private errorMessageService: IErrorMessageService,
@IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService
) {
super(RunJobAction.ID, RunJobAction.LABEL, 'start');
}
public run(context: IJobActionInfo): TPromise<boolean> {
let jobName = context.targetObject.name;
let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.RunAgentJob);
return new TPromise<boolean>((resolve, reject) => {
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => {
if (result.success) {
var startMsg = nls.localize('jobSuccessfullyStarted', ': The job was successfully started.');
this.notificationService.info(jobName+startMsg);
refreshAction.run(context);
resolve(true);
} else {
this.errorMessageService.showDialog(Severity.Error, errorLabel, result.errorMessage);
resolve(false);
}
});
});
}
}
export class StopJobAction extends Action {
public static ID = 'jobaction.stopJob';
public static LABEL = nls.localize('jobaction.stop', "Stop");
constructor(
@INotificationService private notificationService: INotificationService,
@IErrorMessageService private errorMessageService: IErrorMessageService,
@IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService
) {
super(StopJobAction.ID, StopJobAction.LABEL, 'stop');
}
public run(context: IJobActionInfo): TPromise<boolean> {
let jobName = context.targetObject.name;
let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.StopAgentJob);
return new TPromise<boolean>((resolve, reject) => {
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Stop).then(result => {
if (result.success) {
refreshAction.run(context);
var stopMsg = nls.localize('jobSuccessfullyStopped', ': The job was successfully stopped.');
this.notificationService.info(jobName+stopMsg);
resolve(true);
} else {
this.errorMessageService.showDialog(Severity.Error, 'Error', result.errorMessage);
resolve(false);
}
});
});
}
}
export class EditJobAction extends Action {
public static ID = 'jobaction.editJob';
public static LABEL = nls.localize('jobaction.editJob', "Edit Job");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(EditJobAction.ID, EditJobAction.LABEL, 'edit');
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
this._commandService.executeCommand(
'agent.openJobDialog',
actionInfo.ownerUri,
actionInfo.targetObject);
return TPromise.as(true);
}
}
export class DeleteJobAction extends Action {
public static ID = 'jobaction.deleteJob';
public static LABEL = nls.localize('jobaction.deleteJob', "Delete Job");
constructor(
@INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteJobAction.ID, DeleteJobAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
let self = this;
let job = actionInfo.targetObject as sqlops.AgentJobInfo;
self._notificationService.prompt(
Severity.Info,
nls.localize('jobaction.deleteJobConfirm,', "Are you sure you'd like to delete the job '{0}'?", job.name),
[{
label: DeleteJobAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJob);
self._jobService.deleteJob(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}",
job.name, result.errorMessage ? result.errorMessage : 'Unknown error');
self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage);
} else {
let successMessage = nls.localize('jobaction.deletedJob', 'The job was successfully deleted');
self._notificationService.info(successMessage);
}
});
}
}, {
label: DeleteAlertAction.CancelLabel,
run: () => { }
}]
);
return TPromise.as(true);
}
}
// Step Actions
export class NewStepAction extends Action {
public static ID = 'jobaction.newStep';
public static LABEL = nls.localize('jobaction.newStep', "New Step");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(NewStepAction.ID, NewStepAction.LABEL, 'newStepIcon');
}
public run(context: JobHistoryComponent): TPromise<boolean> {
let ownerUri = context.ownerUri;
let server = context.serverName;
let jobInfo = context.agentJobInfo;
return new TPromise<boolean>((resolve, reject) => {
resolve(this._commandService.executeCommand('agent.openNewStepDialog', ownerUri, server, jobInfo, null));
});
}
}
export class DeleteStepAction extends Action {
public static ID = 'jobaction.deleteStep';
public static LABEL = nls.localize('jobaction.deleteStep', "Delete Step");
constructor(
@INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteStepAction.ID, DeleteStepAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
let self = this;
let step = actionInfo.targetObject as sqlops.AgentJobStepInfo;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
self._notificationService.prompt(
Severity.Info,
nls.localize('jobaction.deleteStepConfirm,', "Are you sure you'd like to delete the step '{0}'?", step.stepName),
[{
label: DeleteStepAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJobStep);
self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize('jobaction.failedToDeleteStep', "Could not delete step '{0}'.\nError: {1}",
step.stepName, result.errorMessage ? result.errorMessage : 'Unknown error');
self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage);
refreshAction.run(actionInfo);
} else {
let successMessage = nls.localize('jobaction.deletedStep', 'The job step was successfully deleted');
self._notificationService.info(successMessage);
}
});
}
}, {
label: DeleteAlertAction.CancelLabel,
run: () => { }
}]
);
return TPromise.as(true);
}
}
// Alert Actions
export class NewAlertAction extends Action {
public static ID = 'jobaction.newAlert';
public static LABEL = nls.localize('jobaction.newAlert', "New Alert");
constructor(
) {
super(NewAlertAction.ID, NewAlertAction.LABEL, 'newStepIcon');
}
public run(context: AlertsViewComponent): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
context.openCreateAlertDialog();
resolve(true);
} catch (e) {
reject(e);
}
});
}
}
export class EditAlertAction extends Action {
public static ID = 'jobaction.editAlert';
public static LABEL = nls.localize('jobaction.editAlert', "Edit Alert");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(EditAlertAction.ID, EditAlertAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
this._commandService.executeCommand(
'agent.openAlertDialog',
actionInfo.ownerUri,
actionInfo.targetObject);
return TPromise.as(true);
}
}
export class DeleteAlertAction extends Action {
public static ID = 'jobaction.deleteAlert';
public static LABEL = nls.localize('jobaction.deleteAlert', "Delete Alert");
public static CancelLabel = nls.localize('jobaction.Cancel', "Cancel");
constructor(
@INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteAlertAction.ID, DeleteAlertAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
let self = this;
let alert = actionInfo.targetObject as sqlops.AgentAlertInfo;
self._notificationService.prompt(
Severity.Info,
nls.localize('jobaction.deleteAlertConfirm,', "Are you sure you'd like to delete the alert '{0}'?", alert.name),
[{
label: DeleteAlertAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentAlert);
self._jobService.deleteAlert(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}",
alert.name, result.errorMessage ? result.errorMessage : 'Unknown error');
self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage);
} else {
let successMessage = nls.localize('jobaction.deletedAlert', 'The alert was successfully deleted');
self._notificationService.info(successMessage);
}
});
}
}, {
label: DeleteAlertAction.CancelLabel,
run: () => { }
}]
);
return TPromise.as(true);
}
}
// Operator Actions
export class NewOperatorAction extends Action {
public static ID = 'jobaction.newOperator';
public static LABEL = nls.localize('jobaction.newOperator', "New Operator");
constructor(
) {
super(NewOperatorAction.ID, NewOperatorAction.LABEL, 'newStepIcon');
}
public run(context: OperatorsViewComponent): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
context.openCreateOperatorDialog();
resolve(true);
} catch (e) {
reject(e);
}
});
}
}
export class EditOperatorAction extends Action {
public static ID = 'jobaction.editAlert';
public static LABEL = nls.localize('jobaction.editOperator', "Edit Operator");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(EditOperatorAction.ID, EditOperatorAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
this._commandService.executeCommand(
'agent.openOperatorDialog',
actionInfo.ownerUri,
actionInfo.targetObject);
return TPromise.as(true);
}
}
export class DeleteOperatorAction extends Action {
public static ID = 'jobaction.deleteOperator';
public static LABEL = nls.localize('jobaction.deleteOperator', "Delete Operator");
constructor(
@INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteOperatorAction.ID, DeleteOperatorAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
const self = this;
let operator = actionInfo.targetObject as sqlops.AgentOperatorInfo;
self._notificationService.prompt(
Severity.Info,
nls.localize('jobaction.deleteOperatorConfirm,', "Are you sure you'd like to delete the operator '{0}'?", operator.name),
[{
label: DeleteOperatorAction.LABEL,
run: () => {
self._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator);
self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}",
operator.name, result.errorMessage ? result.errorMessage : 'Unknown error');
self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage);
} else {
let successMessage = nls.localize('joaction.deletedOperator', 'The operator was deleted successfully');
self._notificationService.info(successMessage);
}
});
}
}, {
label: DeleteAlertAction.CancelLabel,
run: () => { }
}]
);
return TPromise.as(true);
}
}
// Proxy Actions
export class NewProxyAction extends Action {
public static ID = 'jobaction.newProxy';
public static LABEL = nls.localize('jobaction.newProxy', "New Proxy");
constructor(
) {
super(NewProxyAction.ID, NewProxyAction.LABEL, 'newStepIcon');
}
public run(context: ProxiesViewComponent): TPromise<boolean> {
return new TPromise<boolean>((resolve, reject) => {
try {
context.openCreateProxyDialog();
resolve(true);
} catch (e) {
reject(e);
}
});
}
}
export class EditProxyAction extends Action {
public static ID = 'jobaction.editProxy';
public static LABEL = nls.localize('jobaction.editProxy', "Edit Proxy");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(EditProxyAction.ID, EditProxyAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
this._commandService.executeCommand(
'agent.openProxyDialog',
actionInfo.ownerUri,
actionInfo.targetObject);
return TPromise.as(true);
}
}
export class DeleteProxyAction extends Action {
public static ID = 'jobaction.deleteProxy';
public static LABEL = nls.localize('jobaction.deleteProxy', "Delete Proxy");
constructor(
@INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteProxyAction.ID, DeleteProxyAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
let self = this;
let proxy = actionInfo.targetObject as sqlops.AgentProxyInfo;
self._notificationService.prompt(
Severity.Info,
nls.localize('jobaction.deleteProxyConfirm,', "Are you sure you'd like to delete the proxy '{0}'?", proxy.accountName),
[{
label: DeleteProxyAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentProxy);
self._jobService.deleteProxy(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}",
proxy.accountName, result.errorMessage ? result.errorMessage : 'Unknown error');
self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage);
} else {
let successMessage = nls.localize('jobaction.deletedProxy', 'The proxy was deleted successfully');
self._notificationService.info(successMessage);
}
});
}
}, {
label: DeleteAlertAction.CancelLabel,
run: () => { }
}]
);
return TPromise.as(true);
}
}

View File

@@ -0,0 +1,370 @@
/*---------------------------------------------------------------------------------------------
* 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';
import * as sqlops from 'sqlops';
import { TPromise } from 'vs/base/common/winjs.base';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { Event, Emitter } from 'vs/base/common/event';
export class JobManagementService implements IJobManagementService {
_serviceBrand: any;
private _onDidChange = new Emitter<void>();
public readonly onDidChange: Event<void> = this._onDidChange.event;
private _providers: { [handle: string]: sqlops.AgentServicesProvider; } = Object.create(null);
private _jobCacheObjectMap: { [server: string]: JobCacheObject; } = {};
private _operatorsCacheObjectMap: { [server: string]: OperatorsCacheObject; } = {};
private _alertsCacheObject: { [server: string]: AlertsCacheObject; } = {};
private _proxiesCacheObjectMap: { [server: string]: ProxiesCacheObject; } = {};
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService
) {
}
public fireOnDidChange(): void {
this._onDidChange.fire(void 0);
}
// Jobs
public getJobs(connectionUri: string): Thenable<sqlops.AgentJobsResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getJobs(connectionUri);
});
}
public deleteJob(connectionUri: string, job: sqlops.AgentJobInfo): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteJob(connectionUri, job);
});
}
public getJobHistory(connectionUri: string, jobID: string, jobName: string): Thenable<sqlops.AgentJobHistoryResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getJobHistory(connectionUri, jobID, jobName);
});
}
public jobAction(connectionUri: string, jobName: string, action: string): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.jobAction(connectionUri, jobName, action);
});
}
// Steps
public deleteJobStep(connectionUri: string, stepInfo: sqlops.AgentJobStepInfo): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteJobStep(connectionUri, stepInfo);
});
}
// Alerts
public getAlerts(connectionUri: string): Thenable<sqlops.AgentAlertsResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getAlerts(connectionUri);
});
}
public deleteAlert(connectionUri: string, alert: sqlops.AgentAlertInfo): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteAlert(connectionUri, alert);
});
}
// Operators
public getOperators(connectionUri: string): Thenable<sqlops.AgentOperatorsResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getOperators(connectionUri);
});
}
public deleteOperator(connectionUri: string, operator: sqlops.AgentOperatorInfo): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteOperator(connectionUri, operator);
});
}
// Proxies
public getProxies(connectionUri: string): Thenable<sqlops.AgentProxiesResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getProxies(connectionUri);
});
}
public deleteProxy(connectionUri: string, proxy: sqlops.AgentProxyInfo): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteProxy(connectionUri, proxy);
});
}
public getCredentials(connectionUri: string): Thenable<sqlops.GetCredentialsResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getCredentials(connectionUri);
});
}
private _runAction<T>(uri: string, action: (handler: sqlops.AgentServicesProvider) => 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 JobManagementService")));
}
let handler = this._providers[providerId];
if (handler) {
return action(handler);
} else {
return TPromise.wrapError(new Error(localize('noHandlerRegistered', "No Handler Registered")));
}
}
public registerProvider(providerId: string, provider: sqlops.AgentServicesProvider): void {
this._providers[providerId] = provider;
}
public get jobCacheObjectMap(): { [server: string]: JobCacheObject; } {
return this._jobCacheObjectMap;
}
public get alertsCacheObjectMap(): { [server: string]: AlertsCacheObject; } {
return this._alertsCacheObject;
}
public get proxiesCacheObjectMap(): { [server: string]: ProxiesCacheObject; } {
return this._proxiesCacheObjectMap;
}
public get operatorsCacheObjectMap(): { [server: string]: OperatorsCacheObject } {
return this._operatorsCacheObjectMap;
}
public addToCache(server: string, cacheObject: JobCacheObject | OperatorsCacheObject | ProxiesCacheObject | AlertsCacheObject) {
if (cacheObject instanceof JobCacheObject) {
this._jobCacheObjectMap[server] = cacheObject;
} else if (cacheObject instanceof OperatorsCacheObject) {
this._operatorsCacheObjectMap[server] = cacheObject;
} else if (cacheObject instanceof AlertsCacheObject) {
this._alertsCacheObject[server] = cacheObject;
} else if (cacheObject instanceof ProxiesCacheObject) {
this._proxiesCacheObjectMap[server] = cacheObject;
}
}
}
/**
* Server level caching of jobs/job histories and their views
*/
export class JobCacheObject {
_serviceBrand: any;
private _jobs: sqlops.AgentJobInfo[] = [];
private _jobHistories: { [jobID: string]: sqlops.AgentJobHistoryInfo[]; } = {};
private _jobSteps: { [jobID: string]: sqlops.AgentJobStepInfo[]; } = {};
private _jobAlerts: { [jobID: string]: sqlops.AgentAlertInfo[]; } = {};
private _jobSchedules: { [jobID: string]: sqlops.AgentJobScheduleInfo[]; } = {};
private _runCharts: { [jobID: string]: string[]; } = {};
private _prevJobID: string;
private _serverName: string;
private _dataView: Slick.Data.DataView<any>;
/* Getters */
public get jobs(): sqlops.AgentJobInfo[] {
return this._jobs;
}
public get jobHistories(): { [jobID: string]: sqlops.AgentJobHistoryInfo[] } {
return this._jobHistories;
}
public get prevJobID(): string {
return this._prevJobID;
}
public getJobHistory(jobID: string): sqlops.AgentJobHistoryInfo[] {
return this._jobHistories[jobID];
}
public get serverName(): string {
return this._serverName;
}
public get dataView(): Slick.Data.DataView<any> {
return this._dataView;
}
public getRunChart(jobID: string): string[] {
return this._runCharts[jobID];
}
public getJobSteps(jobID: string): sqlops.AgentJobStepInfo[] {
return this._jobSteps[jobID];
}
public getJobAlerts(jobID: string): sqlops.AgentAlertInfo[] {
return this._jobAlerts[jobID];
}
public getJobSchedules(jobID: string): sqlops.AgentJobScheduleInfo[] {
return this._jobSchedules[jobID];
}
/* Setters */
public set jobs(value: sqlops.AgentJobInfo[]) {
this._jobs = value;
}
public set jobHistories(value: { [jobID: string]: sqlops.AgentJobHistoryInfo[]; }) {
this._jobHistories = value;
}
public set prevJobID(value: string) {
this._prevJobID = value;
}
public setJobHistory(jobID: string, value: sqlops.AgentJobHistoryInfo[]) {
this._jobHistories[jobID] = value;
}
public setRunChart(jobID: string, value: string[]) {
this._runCharts[jobID] = value;
}
public set serverName(value: string) {
this._serverName = value;
}
public set dataView(value: Slick.Data.DataView<any>) {
this._dataView = value;
}
public setJobSteps(jobID: string, value: sqlops.AgentJobStepInfo[]) {
this._jobSteps[jobID] = value;
}
public setJobAlerts(jobID: string, value: sqlops.AgentAlertInfo[]) {
this._jobAlerts[jobID] = value;
}
public setJobSchedules(jobID: string, value: sqlops.AgentJobScheduleInfo[]) {
this._jobSchedules[jobID] = value;
}
}
/**
* Server level caching of Operators
*/
export class OperatorsCacheObject {
_serviceBrand: any;
private _operators: sqlops.AgentOperatorInfo[];
private _dataView: Slick.Data.DataView<any>;
private _serverName: string;
/** Getters */
public get operators(): sqlops.AgentOperatorInfo[] {
return this._operators;
}
public get dataview(): Slick.Data.DataView<any> {
return this._dataView;
}
public get serverName(): string {
return this._serverName;
}
/** Setters */
public set operators(value: sqlops.AgentOperatorInfo[]) {
this._operators = value;
}
public set dataview(value: Slick.Data.DataView<any>) {
this._dataView = value;
}
public set serverName(value: string) {
this._serverName = value;
}
}
/*
* Server level caching of job alerts and the alerts view
*/
export class AlertsCacheObject {
_serviceBrand: any;
private _alerts: sqlops.AgentAlertInfo[];
private _dataView: Slick.Data.DataView<any>;
private _serverName: string;
/** Getters */
public get alerts(): sqlops.AgentAlertInfo[] {
return this._alerts;
}
public get dataview(): Slick.Data.DataView<any> {
return this._dataView;
}
public get serverName(): string {
return this._serverName;
}
/** Setters */
public set alerts(value: sqlops.AgentAlertInfo[]) {
this._alerts = value;
}
public set dataview(value: Slick.Data.DataView<any>) {
this._dataView = value;
}
public set serverName(value: string) {
this._serverName = value;
}
}
/**
* Server level caching of job proxies and proxies view
*/
export class ProxiesCacheObject {
_serviceBrand: any;
private _proxies: sqlops.AgentProxyInfo[];
private _dataView: Slick.Data.DataView<any>;
private _serverName: string;
/**
* Getters
*/
public get proxies(): sqlops.AgentProxyInfo[] {
return this._proxies;
}
public get dataview(): Slick.Data.DataView<any> {
return this._dataView;
}
public get serverName(): string {
return this._serverName;
}
/** Setters */
public set proxies(value: sqlops.AgentProxyInfo[]) {
this._proxies = value;
}
public set dataview(value: Slick.Data.DataView<any>) {
this._dataView = value;
}
public set serverName(value: string) {
this._serverName = value;
}
}

View File

@@ -0,0 +1,151 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vs/nls';
export class JobManagementUtilities {
public static startIconClass: string = 'action-label icon runJobIcon';
public static stopIconClass: string = 'action-label icon stopJobIcon';
public static jobMessageLength: number = 110;
public static convertToStatusString(status: number): string {
switch (status) {
case (0): return nls.localize('agentUtilities.failed', 'Failed');
case (1): return nls.localize('agentUtilities.succeeded', 'Succeeded');
case (2): return nls.localize('agentUtilities.retry', 'Retry');
case (3): return nls.localize('agentUtilities.canceled', 'Cancelled');
case (4): return nls.localize('agentUtilities.inProgress', 'In Progress');
case (5): return nls.localize('agentUtilities.statusUnknown', 'Status Unknown');
default: return nls.localize('agentUtilities.statusUnknown', 'Status Unknown');
}
}
public static convertToExecutionStatusString(status: number): string {
switch (status) {
case (1): return nls.localize('agentUtilities.executing', 'Executing');
case (2): return nls.localize('agentUtilities.waitingForThread', 'Waiting for Thread');
case (3): return nls.localize('agentUtilities.betweenRetries', 'Between Retries');
case (4): return nls.localize('agentUtilities.idle', 'Idle');
case (5): return nls.localize('agentUtilities.suspended', 'Suspended');
case (6): return nls.localize('agentUtilities.obsolete', '[Obsolete]');
case (7): return 'PerformingCompletionActions';
default: return nls.localize('agentUtilities.statusUnknown', 'Status Unknown');
}
}
public static convertToResponse(bool: boolean) {
return bool ? nls.localize('agentUtilities.yes', 'Yes') : nls.localize('agentUtilities.no', 'No');
}
public static convertToNextRun(date: string) {
if (date.includes('1/1/0001')) {
return nls.localize('agentUtilities.notScheduled', 'Not Scheduled');
} else {
return date;
}
}
public static convertToLastRun(date: string) {
if (date.includes('1/1/0001')) {
return nls.localize('agentUtilities.neverRun', 'Never Run');
} else {
return date;
}
}
public static setRunnable(icon: HTMLElement, index: number) {
if (icon.className.includes('non-runnable')) {
icon.className = icon.className.slice(0, index);
}
}
public static getActionIconClassName(startIcon: HTMLElement, stopIcon: HTMLElement, executionStatus: number) {
this.setRunnable(startIcon, JobManagementUtilities.startIconClass.length);
this.setRunnable(stopIcon, JobManagementUtilities.stopIconClass.length);
switch (executionStatus) {
case (1): // executing
startIcon.className += ' non-runnable';
return;
case (2): // Waiting for thread
startIcon.className += ' non-runnable';
return;
case (3): // Between retries
startIcon.className += ' non-runnable';
return;
case (4): //Idle
stopIcon.className += ' non-runnable';
return;
case (5): // Suspended
stopIcon.className += ' non-runnable';
return;
case (6): //obsolete
startIcon.className += ' non-runnable';
stopIcon.className += ' non-runnable';
return;
case (7): //Performing Completion Actions
startIcon.className += ' non-runnable';
return;
default:
return;
}
}
public static convertDurationToSeconds(duration: string): number {
let split = duration.split(':');
let seconds = (+split[0]) * 60 * 60 + (+split[1]) * 60 + (+split[2]);
return seconds;
}
public static convertColFieldToName(colField: string) {
switch (colField) {
case ('name'):
return 'Name';
case ('lastRun'):
return 'Last Run';
case ('nextRun'):
return 'Next Run';
case ('enabled'):
return 'Enabled';
case ('status'):
return 'Status';
case ('category'):
return 'Category';
case ('runnable'):
return 'Runnable';
case ('schedule'):
return 'Schedule';
case ('lastRunOutcome'):
return 'Last Run Outcome';
}
return '';
}
public static convertColNameToField(columnName: string) {
switch (columnName) {
case ('Name'):
return 'name';
case ('Last Run'):
return 'lastRun';
case ('Next Run'):
return 'nextRun';
case ('Enabled'):
return 'enabled';
case ('Status'):
return 'status';
case ('Category'):
return 'category';
case ('Runnable'):
return 'runnable';
case ('Schedule'):
return 'schedule';
case ('Last Run Outcome'):
return 'lastRunOutcome';
}
return '';
}
}

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import * as sqlops from 'sqlops';
export const SERVICE_ID = 'metadataService';
export const IMetadataService = createDecorator<IMetadataService>(SERVICE_ID);
export interface IMetadataService {
_serviceBrand: any;
getMetadata(connectionUri: string): Thenable<sqlops.ProviderMetadata>;
getDatabaseNames(connectionUri: string): Thenable<string[]>;
getTableInfo(connectionUri: string, metadata: sqlops.ObjectMetadata): Thenable<sqlops.ColumnMetadata[]>;
getViewInfo(connectionUri: string, metadata: sqlops.ObjectMetadata): Thenable<sqlops.ColumnMetadata[]>;
/**
* Register a metadata provider
*/
registerProvider(providerId: string, provider: sqlops.MetadataProvider): void;
}
export class MetadataService implements IMetadataService {
public _serviceBrand: any;
private _disposables: IDisposable[] = [];
private _providers: { [handle: string]: sqlops.MetadataProvider; } = Object.create(null);
constructor( @IConnectionManagementService private _connectionService: IConnectionManagementService) {
}
public getMetadata(connectionUri: string): Thenable<sqlops.ProviderMetadata> {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
let provider = this._providers[providerId];
if (provider) {
return provider.getMetadata(connectionUri);
}
}
return Promise.resolve(undefined);
}
public getDatabaseNames(connectionUri: string): Thenable<string[]> {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
let provider = this._providers[providerId];
if (provider) {
return provider.getDatabases(connectionUri);
}
}
return Promise.resolve([]);
}
public getTableInfo(connectionUri: string, metadata: sqlops.ObjectMetadata): Thenable<sqlops.ColumnMetadata[]> {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
let provider = this._providers[providerId];
if (provider) {
return provider.getTableInfo(connectionUri, metadata);
}
}
return Promise.resolve(undefined);
}
public getViewInfo(connectionUri: string, metadata: sqlops.ObjectMetadata): Thenable<sqlops.ColumnMetadata[]> {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
let provider = this._providers[providerId];
if (provider) {
return provider.getViewInfo(connectionUri, metadata);
}
}
return Promise.resolve(undefined);
}
/**
* Register a metadata provider
*/
public registerProvider(providerId: string, provider: sqlops.MetadataProvider): void {
this._providers[providerId] = provider;
}
public dispose(): void {
this._disposables = dispose(this._disposables);
}
}

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { IItemConfig, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IComponentEventArgs } from 'sql/parts/modelComponents/interfaces';
import { Event } from 'vs/base/common/event';
export interface IView {
readonly id: string;
readonly connection: sqlops.connection.Connection;
readonly serverInfo: sqlops.ServerInfo;
}
export interface IModelViewEventArgs extends IComponentEventArgs {
isRootComponent: boolean;
}
export interface IModelView extends IView {
initializeModel(rootComponent: IComponentShape, validationCallback?: (componentId: string) => Thenable<boolean>): void;
clearContainer(componentId: string): void;
addToContainer(containerId: string, item: IItemConfig, index?: number): void;
removeFromContainer(containerId: string, item: IItemConfig): void;
setLayout(componentId: string, layout: any): void;
setProperties(componentId: string, properties: { [key: string]: any }): void;
setDataProvider(handle: number, componentId: string, context: any): void;
refreshDataProvider(componentId: string, item: any): void;
registerEvent(componentId: string);
onEvent: Event<IModelViewEventArgs>;
validate(componentId: string): Thenable<boolean>;
readonly onDestroy: Event<void>;
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* 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 { IModelView } from 'sql/platform/model/common/modelViewService';
export const SERVICE_ID = 'modelViewService';
export interface IModelViewService {
_serviceBrand: any;
onRegisteredModelView: Event<IModelView>;
registerModelView(widget: IModelView);
}
export const IModelViewService = createDecorator<IModelViewService>(SERVICE_ID);

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* 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 { IModelViewService } from 'sql/platform/modelComponents/common/modelViewService';
import { Event, Emitter } from 'vs/base/common/event';
import { IModelView } from 'sql/platform/model/common/modelViewService';
export class ModelViewService implements IModelViewService {
_serviceBrand: any;
private _onRegisteredModelView = new Emitter<IModelView>();
public readonly onRegisteredModelView: Event<IModelView> = this._onRegisteredModelView.event;
public registerModelView(view: IModelView) {
this._onRegisteredModelView.fire(view);
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/paths';
import * as os from 'os';
import URI from 'vs/base/common/uri';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { Schemas } from 'vs/base/common/network';
export const FILE_SCHEMA: string = 'file';
export function resolveCurrentDirectory(uri: string, rootPath: string): string {
let sqlUri = URI.parse(uri);
let currentDirectory: string;
// use current directory of the sql file if sql file is saved
if (sqlUri.scheme === FILE_SCHEMA) {
currentDirectory = path.dirname(sqlUri.fsPath);
} else if (sqlUri.scheme === Schemas.untitled) {
// if sql file is unsaved/untitled but a workspace is open use workspace root
let root = rootPath;
if (root) {
currentDirectory = root;
} else {
// use temp directory
currentDirectory = os.tmpdir();
}
} else {
currentDirectory = path.dirname(sqlUri.path);
}
return currentDirectory;
}
export function resolveFilePath(uri: string, filePath: string, rootPath: string): string {
let currentDirectory = resolveCurrentDirectory(uri, rootPath);
return path.normalize(path.join(currentDirectory, filePath));
}
export function getRootPath(contextService: IWorkspaceContextService): string {
let isWorkspace = contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
if (isWorkspace) {
let folder = contextService.getWorkspace().folders[0];
if (folder && folder.uri) {
return folder.uri.fsPath;
}
}
return undefined;
}

View File

@@ -0,0 +1,405 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as ConnectionConstants from 'sql/platform/connection/common/constants';
import * as Constants from 'sql/parts/query/common/constants';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { SaveResultsRequestParams } from 'sqlops';
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
import { ISaveRequest, SaveFormat } from 'sql/parts/grid/common/interfaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IOutputService, IOutputChannel, IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/parts/output/common/output';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWindowsService, IWindowService, FileFilter } from 'vs/platform/windows/common/windows';
import { Registry } from 'vs/platform/registry/common/platform';
import URI from 'vs/base/common/uri';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { Schemas } from 'vs/base/common/network';
import * as paths from 'vs/base/common/paths';
import * as nls from 'vs/nls';
import * as pretty from 'pretty-data';
import Severity from 'vs/base/common/severity';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { getBaseLabel } from 'vs/base/common/labels';
import { ShowFileInFolderAction, OpenFileInFolderAction } from 'sql/workbench/common/workspaceActions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { getRootPath, resolveCurrentDirectory, resolveFilePath } from 'sql/platform/node/pathUtilities';
let prevSavePath: string;
/**
* Handles save results request from the context menu of slickGrid
*/
export class ResultSerializer {
public static tempFileCount: number = 1;
private static MAX_FILENAMES = 100;
private _uri: string;
private _filePath: string;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IOutputService private _outputService: IOutputService,
@IQueryManagementService private _queryManagementService: IQueryManagementService,
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
@IEditorService private _editorService: IEditorService,
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
@IWindowsService private _windowsService: IWindowsService,
@IWindowService private _windowService: IWindowService,
@IUntitledEditorService private _untitledEditorService: IUntitledEditorService,
@INotificationService private _notificationService: INotificationService
) { }
/**
* Handle save request by getting filename from user and sending request to service
*/
public saveResults(uri: string, saveRequest: ISaveRequest): Thenable<void> {
const self = this;
this._uri = uri;
// prompt for filepath
return self.promptForFilepath(saveRequest).then(filePath => {
if (filePath) {
return self.sendRequestToService(filePath, saveRequest.batchIndex, saveRequest.resultSetNumber, saveRequest.format, saveRequest.selection ? saveRequest.selection[0] : undefined);
}
return Promise.resolve(undefined);
});
}
/**
* Open a xml/json link - Opens the content in a new editor pane
*/
public openLink(content: string, columnName: string, linkType: string): void {
let fileMode: string = undefined;
let fileUri = this.getUntitledFileUri(columnName);
if (linkType === SaveFormat.XML) {
fileMode = SaveFormat.XML;
try {
content = pretty.pd.xml(content);
} catch (e) {
// If Xml fails to parse, fall back on original Xml content
}
} else if (linkType === SaveFormat.JSON) {
let jsonContent: string = undefined;
fileMode = SaveFormat.JSON;
try {
jsonContent = JSON.parse(content);
} catch (e) {
// If Json fails to parse, fall back on original Json content
}
if (jsonContent) {
// If Json content was valid and parsed, pretty print content to a string
content = JSON.stringify(jsonContent, undefined, 4);
}
}
this.openUntitledFile(fileMode, content, fileUri);
}
private getUntitledFileUri(columnName: string): URI {
let fileName = columnName;
let uri: URI = URI.from({ scheme: Schemas.untitled, path: fileName });
// If the current filename is taken, try another up to a max number
if (this._untitledEditorService.exists(uri)) {
let i = 1;
while (i < ResultSerializer.MAX_FILENAMES
&& this._untitledEditorService.exists(uri)) {
fileName = [columnName, i.toString()].join('-');
uri = URI.from({ scheme: Schemas.untitled, path: fileName });
i++;
}
if (this._untitledEditorService.exists(uri)) {
// If this fails, return undefined and let the system figure out the right name
uri = undefined;
}
}
return uri;
}
private ensureOutputChannelExists(): void {
Registry.as<IOutputChannelRegistry>(OutputExtensions.OutputChannels)
.registerChannel(ConnectionConstants.outputChannelName, ConnectionConstants.outputChannelName);
}
private get outputChannel(): IOutputChannel {
this.ensureOutputChannelExists();
return this._outputService.getChannel(ConnectionConstants.outputChannelName);
}
private get rootPath(): string {
return getRootPath(this._contextService);
}
private logToOutputChannel(message: string): void {
this.outputChannel.append(message);
}
private promptForFilepath(saveRequest: ISaveRequest): Thenable<string> {
let filepathPlaceHolder = (prevSavePath) ? paths.dirname(prevSavePath) : resolveCurrentDirectory(this._uri, this.rootPath);
filepathPlaceHolder = paths.join(filepathPlaceHolder, this.getResultsDefaultFilename(saveRequest));
return this._windowService.showSaveDialog({
title: nls.localize('resultsSerializer.saveAsFileTitle', 'Choose Results File'),
defaultPath: paths.normalize(filepathPlaceHolder, true),
filters: this.getResultsFileExtension(saveRequest)
}).then(filePath => {
prevSavePath = filePath;
return Promise.resolve(filePath);
});
}
private getResultsDefaultFilename(saveRequest: ISaveRequest): string {
let fileName = 'Results';
switch (saveRequest.format) {
case SaveFormat.CSV:
fileName = fileName + '.csv';
break;
case SaveFormat.JSON:
fileName = fileName + '.json';
break;
case SaveFormat.EXCEL:
fileName = fileName + '.xlsx';
break;
case SaveFormat.XML:
fileName = fileName + '.xml';
break;
default:
fileName = fileName + '.txt';
}
return fileName;
}
private getResultsFileExtension(saveRequest: ISaveRequest): FileFilter[] {
let fileFilters = new Array<FileFilter>();
let fileFilter: { extensions: string[]; name: string } = { extensions: undefined, name: undefined };
switch (saveRequest.format) {
case SaveFormat.CSV:
fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionCSVTitle', 'CSV (Comma delimited)');
fileFilter.extensions = ['csv'];
break;
case SaveFormat.JSON:
fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionJSONTitle', 'JSON');
fileFilter.extensions = ['json'];
break;
case SaveFormat.EXCEL:
fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionExcelTitle', 'Excel Workbook');
fileFilter.extensions = ['xlsx'];
break;
case SaveFormat.XML:
fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionXMLTitle', 'XML');
fileFilter.extensions = ['xml'];
break;
default:
fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionTXTTitle', 'Plain Text');
fileFilter.extensions = ['txt'];
}
fileFilters.push(fileFilter);
return fileFilters;
}
private getConfigForCsv(): SaveResultsRequestParams {
let saveResultsParams = <SaveResultsRequestParams>{ resultFormat: SaveFormat.CSV as string };
// get save results config from vscode config
let saveConfig = WorkbenchUtils.getSqlConfigSection(this._workspaceConfigurationService, Constants.configSaveAsCsv);
// if user entered config, set options
if (saveConfig) {
if (saveConfig.includeHeaders !== undefined) {
saveResultsParams.includeHeaders = saveConfig.includeHeaders;
}
if (saveConfig.delimiter !== undefined) {
saveResultsParams.delimiter = saveConfig.delimiter;
}
if (saveConfig.lineSeperator !== undefined) {
saveResultsParams.lineSeperator = saveConfig.lineSeperator;
}
if (saveConfig.textIdentifier !== undefined) {
saveResultsParams.textIdentifier = saveConfig.textIdentifier;
}
if (saveConfig.encoding !== undefined) {
saveResultsParams.encoding = saveConfig.encoding;
}
}
return saveResultsParams;
}
private getConfigForJson(): SaveResultsRequestParams {
// JSON does not currently have special conditions
let saveResultsParams = <SaveResultsRequestParams>{ resultFormat: SaveFormat.JSON as string };
return saveResultsParams;
}
private getConfigForExcel(): SaveResultsRequestParams {
// get save results config from vscode config
// Note: we are currently using the configSaveAsCsv setting since it has the option mssql.saveAsCsv.includeHeaders
// and we want to have just 1 setting that lists this.
let config = this.getConfigForCsv();
config.resultFormat = SaveFormat.EXCEL;
config.delimiter = undefined;
config.lineSeperator = undefined;
config.textIdentifier = undefined;
config.encoding = undefined;
return config;
}
private getConfigForXml(): SaveResultsRequestParams {
let saveResultsParams = <SaveResultsRequestParams>{ resultFormat: SaveFormat.XML as string };
// get save results config from vscode config
let saveConfig = WorkbenchUtils.getSqlConfigSection(this._workspaceConfigurationService, Constants.configSaveAsXml);
// if user entered config, set options
if (saveConfig) {
if (saveConfig.formatted !== undefined) {
saveResultsParams.formatted = saveConfig.formatted;
}
if (saveConfig.encoding !== undefined) {
saveResultsParams.encoding = saveConfig.encoding;
}
}
return saveResultsParams;
}
private getParameters(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: Slick.Range): SaveResultsRequestParams {
let saveResultsParams: SaveResultsRequestParams;
if (!paths.isAbsolute(filePath)) {
this._filePath = resolveFilePath(this._uri, filePath, this.rootPath);
} else {
this._filePath = filePath;
}
if (format === SaveFormat.CSV) {
saveResultsParams = this.getConfigForCsv();
} else if (format === SaveFormat.JSON) {
saveResultsParams = this.getConfigForJson();
} else if (format === SaveFormat.EXCEL) {
saveResultsParams = this.getConfigForExcel();
} else if (format === SaveFormat.XML) {
saveResultsParams = this.getConfigForXml();
}
saveResultsParams.filePath = this._filePath;
saveResultsParams.ownerUri = this._uri;
saveResultsParams.resultSetIndex = resultSetNo;
saveResultsParams.batchIndex = batchIndex;
if (this.isSelected(selection)) {
saveResultsParams.rowStartIndex = selection.fromRow;
saveResultsParams.rowEndIndex = selection.toRow;
saveResultsParams.columnStartIndex = selection.fromCell;
saveResultsParams.columnEndIndex = selection.toCell;
}
return saveResultsParams;
}
/**
* Check if a range of cells were selected.
*/
private isSelected(selection: Slick.Range): boolean {
return (selection && !((selection.fromCell === selection.toCell) && (selection.fromRow === selection.toRow)));
}
private promptFileSavedNotification(savedFilePath: string) {
let label = getBaseLabel(paths.dirname(savedFilePath));
this._notificationService.prompt(
Severity.Info,
LocalizedConstants.msgSaveSucceeded + savedFilePath,
[{
label: nls.localize('openLocation', "Open file location"),
run: () => {
let action = new ShowFileInFolderAction(savedFilePath, label || paths.sep, this._windowsService);
action.run();
action.dispose();
}
}, {
label: nls.localize('openFile', "Open file"),
run: () => {
let action = new OpenFileInFolderAction(savedFilePath, label || paths.sep, this._windowsService);
action.run();
action.dispose();
}
}]
);
}
/**
* Send request to sql tools service to save a result set
*/
private sendRequestToService(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: Slick.Range): Thenable<void> {
let saveResultsParams = this.getParameters(filePath, batchIndex, resultSetNo, format, selection);
this.logToOutputChannel(LocalizedConstants.msgSaveStarted + this._filePath);
// send message to the sqlserverclient for converting results to the requested format and saving to filepath
return this._queryManagementService.saveResults(saveResultsParams).then(result => {
if (result.messages) {
this._notificationService.notify({
severity: Severity.Error,
message: LocalizedConstants.msgSaveFailed + result.messages
});
this.logToOutputChannel(LocalizedConstants.msgSaveFailed + result.messages);
} else {
this.promptFileSavedNotification(this._filePath);
this.logToOutputChannel(LocalizedConstants.msgSaveSucceeded + filePath);
this.openSavedFile(this._filePath, format);
}
// TODO telemetry for save results
// Telemetry.sendTelemetryEvent('SavedResults', { 'type': format });
}, error => {
this._notificationService.notify({
severity: Severity.Error,
message: LocalizedConstants.msgSaveFailed + error
});
this.logToOutputChannel(LocalizedConstants.msgSaveFailed + error);
});
}
/**
* Open the saved file in a new vscode editor pane
*/
private openSavedFile(filePath: string, format: string): void {
if (format !== SaveFormat.EXCEL) {
let uri = URI.file(filePath);
this._editorService.openEditor({ resource: uri }).then((result) => {
}, (error: any) => {
this._notificationService.notify({
severity: Severity.Error,
message: error
});
});
}
}
/**
* Open the saved file in a new vscode editor pane
*/
private openUntitledFile(fileMode: string, contents: string, fileUri: URI = undefined): void {
const input = this._untitledEditorService.createOrGet(fileUri, fileMode, contents);
this._editorService.openEditor(input, { pinned: true })
.then(
(success) => {
},
(error: any) => {
this._notificationService.notify({
severity: Severity.Error,
message: error
});
}
);
}
}

View 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ISqlOAuthService = createDecorator<ISqlOAuthService>('sqlOAuthService');
/**
* Defines simple electron based utilities for registering and sending IPC to the main thread to
* handle OAuth requests. This is to avoid breaking layering issues caused when certain
* electron-based services are referenced in unit testable code.
*/
export interface ISqlOAuthService {
_serviceBrand: any;
/**
* Sends request to main thread to handle OAuth request
* @param {string} eventId Unique ID of the request to return upon completion
* @param {string} url URL to load to do OAuth
* @param {boolean} silent Whether or not to show the OAuth window
* @return {Thenable<string>} Promise to return an authorization code
*/
performOAuthAuthorization(eventId: string, url: string, silent: boolean): void;
/**
* Registers a handler for the oauth-reply event on the IPC channel
* @param {(event, args) => void} handler Handler to call when the event is triggered
*/
registerOAuthCallback(handler: (event, args) => void): void;
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as electron from 'electron';
import { ISqlOAuthService } from 'sql/platform/oAuth/common/sqlOAuthService';
/**
* Implements simple electron based utilities for registering and sending IPC to the main thread to
* handle OAuth requests. This is to avoid breaking layering issues caused when certain
* electron-based services are referenced in unit testable code.
*/
export class SqlOAuthService implements ISqlOAuthService {
public _serviceBrand: any;
/**
* Sends request to main thread to handle OAuth request
* @param {string} eventId Unique ID of the request to return upon completion
* @param {string} url URL to load to do OAuth
* @param {boolean} silent Whether or not to show the OAuth window
* @return {Thenable<string>} Promise to return an authorization code
*/
performOAuthAuthorization(eventId: string, url: string, silent: boolean): void {
// Setup the args and send the IPC call
electron.ipcRenderer.send(
'oauth',
{
url: url,
silent: silent,
eventId: eventId
}
);
}
/**
* Registers a handler for the oauth-reply event on the IPC channel
* @param {(event, args) => void} handler Handler to call when the event is triggered
*/
registerOAuthCallback(handler: (event, args) => void): void {
electron.ipcRenderer.on('oauth-reply', handler);
}
}

View File

@@ -0,0 +1,326 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as sqlops from 'sqlops';
import { TPromise } from 'vs/base/common/winjs.base';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export const SERVICE_ID = 'queryManagementService';
export const IQueryManagementService = createDecorator<IQueryManagementService>(SERVICE_ID);
export interface IQueryManagementService {
_serviceBrand: any;
addQueryRequestHandler(queryType: string, runner: IQueryRequestHandler): IDisposable;
isProviderRegistered(providerId: string): boolean;
registerRunner(runner: QueryRunner, uri: string): void;
cancelQuery(ownerUri: string): Thenable<sqlops.QueryCancelResult>;
runQuery(ownerUri: string, selection: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void>;
runQueryString(ownerUri: string, queryString: string): Thenable<void>;
runQueryAndReturn(ownerUri: string, queryString: string): Thenable<sqlops.SimpleExecuteResult>;
parseSyntax(ownerUri: string, query: string): Thenable<sqlops.SyntaxParseResult>;
getQueryRows(rowData: sqlops.QueryExecuteSubsetParams): Thenable<sqlops.QueryExecuteSubsetResult>;
disposeQuery(ownerUri: string): Thenable<void>;
saveResults(requestParams: sqlops.SaveResultsRequestParams): Thenable<sqlops.SaveResultRequestResult>;
// Callbacks
onQueryComplete(result: sqlops.QueryExecuteCompleteNotificationResult): void;
onBatchStart(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void;
onBatchComplete(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void;
onResultSetAvailable(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void;
onResultSetUpdated(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void;
onMessage(message: sqlops.QueryExecuteMessageParams): void;
// Edit Data Callbacks
onEditSessionReady(ownerUri: string, success: boolean, message: string): void;
// Edit Data Functions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void>;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(rowData: sqlops.EditSubsetParams): Thenable<sqlops.EditSubsetResult>;
}
/*
* An object that can handle basic request-response actions related to queries
*/
export interface IQueryRequestHandler {
cancelQuery(ownerUri: string): Thenable<sqlops.QueryCancelResult>;
runQuery(ownerUri: string, selection: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void>;
runQueryString(ownerUri: string, queryString: string): Thenable<void>;
runQueryAndReturn(ownerUri: string, queryString: string): Thenable<sqlops.SimpleExecuteResult>;
parseSyntax(ownerUri: string, query: string): Thenable<sqlops.SyntaxParseResult>;
getQueryRows(rowData: sqlops.QueryExecuteSubsetParams): Thenable<sqlops.QueryExecuteSubsetResult>;
disposeQuery(ownerUri: string): Thenable<void>;
saveResults(requestParams: sqlops.SaveResultsRequestParams): Thenable<sqlops.SaveResultRequestResult>;
// Edit Data actions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void>;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(rowData: sqlops.EditSubsetParams): Thenable<sqlops.EditSubsetResult>;
}
export class QueryManagementService implements IQueryManagementService {
public _serviceBrand: any;
private _requestHandlers = new Map<string, IQueryRequestHandler>();
// public for testing only
public _queryRunners = new Map<string, QueryRunner>();
// public for testing only
public _handlerCallbackQueue: ((run: QueryRunner) => void)[] = [];
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
}
// Registers queryRunners with their uris to distribute notifications.
// Ensures that notifications are handled in the correct order by handling
// enqueued handlers first.
// public for testing only
public registerRunner(runner: QueryRunner, uri: string): void {
// If enqueueOrRun was called before registerRunner for the current query,
// _handlerCallbackQueue will be non-empty. Run all handlers in the queue first
// so that notifications are handled in order they arrived
while (this._handlerCallbackQueue.length > 0) {
let handler = this._handlerCallbackQueue.shift();
handler(runner);
}
// Set the runner for any other handlers if the runner is in use by the
// current query or a subsequent query
if (!runner.hasCompleted) {
this._queryRunners.set(uri, runner);
}
}
// Handles logic to run the given handlerCallback at the appropriate time. If the given runner is
// undefined, the handlerCallback is put on the _handlerCallbackQueue to be run once the runner is set
// public for testing only
private enqueueOrRun(handlerCallback: (runnerParam: QueryRunner) => void, runner: QueryRunner): void {
if (runner === undefined) {
this._handlerCallbackQueue.push(handlerCallback);
} else {
handlerCallback(runner);
}
}
private _notify(ownerUri: string, sendNotification: (runner: QueryRunner) => void): void {
let runner = this._queryRunners.get(ownerUri);
this.enqueueOrRun(sendNotification, runner);
}
public addQueryRequestHandler(queryType: string, handler: IQueryRequestHandler): IDisposable {
this._requestHandlers.set(queryType, handler);
return {
dispose: () => {
}
};
}
public isProviderRegistered(providerId: string): boolean {
let handler = this._requestHandlers.get(providerId);
return !!handler;
}
private addTelemetry(eventName: string, ownerUri: string, runOptions?: sqlops.ExecutionPlanOptions): void {
let providerId: string = this._connectionService.getProviderIdFromUri(ownerUri);
let data: TelemetryUtils.IConnectionTelemetryData = {
provider: providerId,
};
if (runOptions) {
data = Object.assign({}, data, {
displayEstimatedQueryPlan: runOptions.displayEstimatedQueryPlan,
displayActualQueryPlan: runOptions.displayActualQueryPlan
});
}
TelemetryUtils.addTelemetry(this._telemetryService, eventName, data);
}
private _runAction<T>(uri: string, action: (handler: IQueryRequestHandler) => Thenable<T>): Thenable<T> {
let providerId: string = this._connectionService.getProviderIdFromUri(uri);
if (!providerId) {
return TPromise.wrapError(new Error('Connection is required in order to interact with queries'));
}
let handler = this._requestHandlers.get(providerId);
if (handler) {
return action(handler);
} else {
return TPromise.wrapError(new Error('No Handler Registered'));
}
}
public cancelQuery(ownerUri: string): Thenable<sqlops.QueryCancelResult> {
this.addTelemetry(TelemetryKeys.CancelQuery, ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.cancelQuery(ownerUri);
});
}
public runQuery(ownerUri: string, selection: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void> {
this.addTelemetry(TelemetryKeys.RunQuery, ownerUri, runOptions);
return this._runAction(ownerUri, (runner) => {
return runner.runQuery(ownerUri, selection, runOptions);
});
}
public runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void> {
this.addTelemetry(TelemetryKeys.RunQueryStatement, ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.runQueryStatement(ownerUri, line, column);
});
}
public runQueryString(ownerUri: string, queryString: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.runQueryString(ownerUri, queryString);
});
}
public runQueryAndReturn(ownerUri: string, queryString: string): Thenable<sqlops.SimpleExecuteResult> {
return this._runAction(ownerUri, (runner) => {
return runner.runQueryAndReturn(ownerUri, queryString);
});
}
public parseSyntax(ownerUri: string, query: string): Thenable<sqlops.SyntaxParseResult> {
return this._runAction(ownerUri, (runner) => {
return runner.parseSyntax(ownerUri, query);
});
}
public getQueryRows(rowData: sqlops.QueryExecuteSubsetParams): Thenable<sqlops.QueryExecuteSubsetResult> {
return this._runAction(rowData.ownerUri, (runner) => {
return runner.getQueryRows(rowData);
});
}
public disposeQuery(ownerUri: string): Thenable<void> {
this._queryRunners.delete(ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.disposeQuery(ownerUri);
});
}
public saveResults(requestParams: sqlops.SaveResultsRequestParams): Thenable<sqlops.SaveResultRequestResult> {
return this._runAction(requestParams.ownerUri, (runner) => {
return runner.saveResults(requestParams);
});
}
public onQueryComplete(result: sqlops.QueryExecuteCompleteNotificationResult): void {
this._notify(result.ownerUri, (runner: QueryRunner) => {
runner.handleQueryComplete(result);
});
}
public onBatchStart(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void {
this._notify(batchInfo.ownerUri, (runner: QueryRunner) => {
runner.handleBatchStart(batchInfo);
});
}
public onBatchComplete(batchInfo: sqlops.QueryExecuteBatchNotificationParams): void {
this._notify(batchInfo.ownerUri, (runner: QueryRunner) => {
runner.handleBatchComplete(batchInfo);
});
}
public onResultSetAvailable(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._notify(resultSetInfo.ownerUri, (runner: QueryRunner) => {
runner.handleResultSetAvailable(resultSetInfo);
});
}
public onResultSetUpdated(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._notify(resultSetInfo.ownerUri, (runner: QueryRunner) => {
runner.handleResultSetUpdated(resultSetInfo);
});
}
public onMessage(message: sqlops.QueryExecuteMessageParams): void {
this._notify(message.ownerUri, (runner: QueryRunner) => {
runner.handleMessage(message);
});
}
// Edit Data Functions
public initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString);
});
}
public onEditSessionReady(ownerUri: string, success: boolean, message: string): void {
this._notify(ownerUri, (runner: QueryRunner) => {
runner.handleEditSessionReady(ownerUri, success, message);
});
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult> {
return this._runAction(ownerUri, (runner) => {
return runner.updateCell(ownerUri, rowId, columnId, newValue);
});
}
public commitEdit(ownerUri: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.commitEdit(ownerUri);
});
}
public createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult> {
return this._runAction(ownerUri, (runner) => {
return runner.createRow(ownerUri);
});
}
public deleteRow(ownerUri: string, rowId: number): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.deleteRow(ownerUri, rowId);
});
}
public disposeEdit(ownerUri: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.disposeEdit(ownerUri);
});
}
public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult> {
return this._runAction(ownerUri, (runner) => {
return runner.revertCell(ownerUri, rowId, columnId);
});
}
public revertRow(ownerUri: string, rowId: number): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.revertRow(ownerUri, rowId);
});
}
public getEditRows(rowData: sqlops.EditSubsetParams): Thenable<sqlops.EditSubsetResult> {
return this._runAction(rowData.ownerUri, (runner) => {
return runner.getEditRows(rowData);
});
}
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { DataService } from 'sql/parts/grid/services/dataService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import {
ISelectionData,
ResultSetSubset,
EditUpdateCellResult,
EditSessionReadyParams,
EditSubsetResult,
EditCreateRowResult,
EditRevertCellResult,
ExecutionPlanOptions
} from 'sqlops';
import { QueryInfo } from 'sql/platform/query/common/queryModelService';
export const SERVICE_ID = 'queryModelService';
export const IQueryModelService = createDecorator<IQueryModelService>(SERVICE_ID);
/**
* Interface for the logic of handling running queries and grid interactions for all URIs.
*/
export interface IQueryModelService {
_serviceBrand: any;
getQueryRunner(uri: string): QueryRunner;
getConfig(): Promise<{ [key: string]: any }>;
getShortcuts(): Promise<any>;
getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Thenable<ResultSetSubset>;
runQuery(uri: string, selection: ISelectionData, queryInput: QueryInput, runOptions?: ExecutionPlanOptions): void;
runQueryStatement(uri: string, selection: ISelectionData, queryInput: QueryInput): void;
runQueryString(uri: string, selection: string, queryInput: QueryInput);
cancelQuery(input: QueryRunner | string): void;
disposeQuery(uri: string): void;
isRunningQuery(uri: string): boolean;
getDataService(uri: string): DataService;
refreshResultsets(uri: string): void;
sendGridContentEvent(uri: string, eventName: string): void;
resizeResultsets(uri: string): void;
onAngularLoaded(uri: string): void;
copyResults(uri: string, selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void;
setEditorSelection(uri: string, index: number): void;
showWarning(uri: string, message: string): void;
showError(uri: string, message: string): void;
showCommitError(error: string): void;
onRunQueryStart: Event<string>;
onRunQueryComplete: Event<string>;
// Edit Data Functions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): void;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(ownerUri: string, rowStart: number, numberOfRows: number): Thenable<EditSubsetResult>;
_getQueryInfo(uri: string): QueryInfo;
// Edit Data Callbacks
onEditSessionReady: Event<EditSessionReadyParams>;
}

View File

@@ -0,0 +1,607 @@
/*---------------------------------------------------------------------------------------------
* 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 GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import QueryRunner, { EventType as QREvents } from 'sql/platform/query/common/queryRunner';
import { DataService } from 'sql/parts/grid/services/dataService';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import { QueryStatusbarItem } from 'sql/parts/query/execution/queryStatus';
import { SqlFlavorStatusbarItem } from 'sql/parts/query/common/flavorStatus';
import { RowCountStatusBarItem } from 'sql/parts/query/common/rowCountStatus';
import { TimeElapsedStatusBarItem } from 'sql/parts/query/common/timeElapsedStatus';
import * as sqlops from 'sqlops';
import * as nls from 'vs/nls';
import * as statusbar from 'vs/workbench/browser/parts/statusbar/statusbar';
import * as platform from 'vs/platform/registry/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import * as strings from 'vs/base/common/strings';
import * as types from 'vs/base/common/types';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
const selectionSnippetMaxLen = 100;
export interface QueryEvent {
type: string;
data: any;
}
/**
* Holds information about the state of a query runner
*/
export class QueryInfo {
public queryRunner: QueryRunner;
public dataService: DataService;
public queryEventQueue: QueryEvent[];
public selection: Array<sqlops.ISelectionData>;
public queryInput: QueryInput;
public selectionSnippet: string;
// Notes if the angular components have obtained the DataService. If not, all messages sent
// via the data service will be lost.
public dataServiceReady: boolean;
constructor() {
this.dataServiceReady = false;
this.queryEventQueue = [];
this.selection = [];
}
}
/**
* Handles running queries and grid interactions for all URIs. Interacts with each URI's results grid via a DataService instance
*/
export class QueryModelService implements IQueryModelService {
_serviceBrand: any;
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _queryInfoMap: Map<string, QueryInfo>;
private _onRunQueryStart: Emitter<string>;
private _onRunQueryComplete: Emitter<string>;
private _onEditSessionReady: Emitter<sqlops.EditSessionReadyParams>;
// EVENTS /////////////////////////////////////////////////////////////
public get onRunQueryStart(): Event<string> { return this._onRunQueryStart.event; }
public get onRunQueryComplete(): Event<string> { return this._onRunQueryComplete.event; }
public get onEditSessionReady(): Event<sqlops.EditSessionReadyParams> { return this._onEditSessionReady.event; }
// CONSTRUCTOR /////////////////////////////////////////////////////////
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@INotificationService private _notificationService: INotificationService
) {
this._queryInfoMap = new Map<string, QueryInfo>();
this._onRunQueryStart = new Emitter<string>();
this._onRunQueryComplete = new Emitter<string>();
this._onEditSessionReady = new Emitter<sqlops.EditSessionReadyParams>();
// Register Statusbar items
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
TimeElapsedStatusBarItem,
statusbar.StatusbarAlignment.RIGHT,
100 /* Should appear to the right of the SQL editor status */
));
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
RowCountStatusBarItem,
statusbar.StatusbarAlignment.RIGHT,
100 /* Should appear to the right of the SQL editor status */
));
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
QueryStatusbarItem,
statusbar.StatusbarAlignment.RIGHT,
100 /* High Priority */
));
(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
SqlFlavorStatusbarItem,
statusbar.StatusbarAlignment.RIGHT,
90 /* Should appear to the right of the SQL editor status */
));
}
// IQUERYMODEL /////////////////////////////////////////////////////////
public getDataService(uri: string): DataService {
let dataService = this._getQueryInfo(uri).dataService;
if (!dataService) {
throw new Error('Could not find data service for uri: ' + uri);
}
return dataService;
}
/**
* Force all grids to re-render. This is needed to re-render the grids when switching
* between different URIs.
*/
public refreshResultsets(uri: string): void {
this._fireGridContentEvent(uri, GridContentEvents.RefreshContents);
}
/**
* Resize the grid UI to fit the current screen size.
*/
public resizeResultsets(uri: string): void {
this._fireGridContentEvent(uri, GridContentEvents.ResizeContents);
}
public sendGridContentEvent(uri: string, eventName: string): void {
this._fireGridContentEvent(uri, eventName);
}
/**
* To be called by an angular component's DataService when the component has finished loading.
* Sends all previously enqueued query events to the DataService and signals to stop enqueuing
* any further events. This prevents QueryEvents from getting lost if they are sent before
* angular is listening for them.
*/
public onAngularLoaded(uri: string) {
let info = this._getQueryInfo(uri);
info.dataServiceReady = true;
this._sendQueuedEvents(uri);
}
/**
* Get more data rows from the current resultSets from the service layer
*/
public getQueryRows(uri: string, rowStart: number, numberOfRows: number, batchId: number, resultId: number): Thenable<sqlops.ResultSetSubset> {
return this._getQueryInfo(uri).queryRunner.getQueryRows(rowStart, numberOfRows, batchId, resultId).then(results => {
return results.resultSubset;
});
}
public getEditRows(uri: string, rowStart: number, numberOfRows: number): Thenable<sqlops.EditSubsetResult> {
return this._queryInfoMap.get(uri).queryRunner.getEditRows(rowStart, numberOfRows).then(results => {
return results;
});
}
public getConfig(): Promise<{ [key: string]: any }> {
return undefined;
}
public getShortcuts(): Promise<any> {
return undefined;
}
public copyResults(uri: string, selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
this._queryInfoMap.get(uri).queryRunner.copyResults(selection, batchId, resultId, includeHeaders);
}
public setEditorSelection(uri: string, index: number): void {
let info: QueryInfo = this._queryInfoMap.get(uri);
if (info && info.queryInput) {
info.queryInput.updateSelection(info.selection[index]);
}
}
public showWarning(uri: string, message: string): void {
}
public showError(uri: string, message: string): void {
}
public showCommitError(error: string): void {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('commitEditFailed', 'Commit row failed: ') + error
});
}
public isRunningQuery(uri: string): boolean {
return !this._queryInfoMap.has(uri)
? false
: this._getQueryInfo(uri).queryRunner.isExecuting;
}
/**
* Run a query for the given URI with the given text selection
*/
public runQuery(uri: string, selection: sqlops.ISelectionData, queryInput: QueryInput, runOptions?: sqlops.ExecutionPlanOptions): void {
this.doRunQuery(uri, selection, queryInput, false, runOptions);
}
/**
* Run the current SQL statement for the given URI
*/
public runQueryStatement(uri: string, selection: sqlops.ISelectionData, queryInput: QueryInput): void {
this.doRunQuery(uri, selection, queryInput, true);
}
/**
* Run the current SQL statement for the given URI
*/
public runQueryString(uri: string, selection: string, queryInput: QueryInput): void {
this.doRunQuery(uri, selection, queryInput, true);
}
/**
* Run Query implementation
*/
private doRunQuery(uri: string, selection: sqlops.ISelectionData | string, queryInput: QueryInput,
runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): void {
// Reuse existing query runner if it exists
let queryRunner: QueryRunner;
let info: QueryInfo;
if (this._queryInfoMap.has(uri)) {
info = this._getQueryInfo(uri);
let existingRunner: QueryRunner = info.queryRunner;
// If the query is already in progress, don't attempt to send it
if (existingRunner.isExecuting) {
return;
}
// If the query is not in progress, we can reuse the query runner
queryRunner = existingRunner;
info.selection = [];
info.selectionSnippet = undefined;
} else {
// We do not have a query runner for this editor, so create a new one
// and map it to the results uri
info = this.initQueryRunner(uri);
queryRunner = info.queryRunner;
}
info.queryInput = queryInput;
if (types.isString(selection)) {
// Run the query string in this case
if (selection.length < selectionSnippetMaxLen) {
info.selectionSnippet = selection;
} else {
info.selectionSnippet = selection.substring(0, selectionSnippetMaxLen - 3) + '...';
}
queryRunner.runQuery(selection, runOptions);
} else if (runCurrentStatement) {
queryRunner.runQueryStatement(selection);
} else {
queryRunner.runQuery(selection, runOptions);
}
}
private initQueryRunner(uri: string): QueryInfo {
let queryRunner = this._instantiationService.createInstance(QueryRunner, uri);
let info = new QueryInfo();
queryRunner.addListener(QREvents.RESULT_SET, e => {
this._fireQueryEvent(uri, 'resultSet', e);
});
queryRunner.addListener(QREvents.BATCH_START, b => {
let link = undefined;
let messageText = LocalizedConstants.runQueryBatchStartMessage;
if (b.selection) {
if (info.selectionSnippet) {
// This indicates it's a query string. Do not include line information since it'll be inaccurate, but show some of the
// executed query text
messageText = nls.localize('runQueryStringBatchStartMessage', 'Started executing query "{0}"', info.selectionSnippet);
} else {
link = {
text: strings.format(LocalizedConstants.runQueryBatchStartLine, b.selection.startLine + 1)
};
}
}
let message = {
message: messageText,
batchId: b.id,
isError: false,
time: new Date().toLocaleTimeString(),
link: link
};
this._fireQueryEvent(uri, 'message', message);
info.selection.push(this._validateSelection(b.selection));
});
queryRunner.addListener(QREvents.MESSAGE, m => {
this._fireQueryEvent(uri, 'message', m);
});
queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => {
this._onRunQueryComplete.fire(uri);
this._fireQueryEvent(uri, 'complete', totalMilliseconds);
});
queryRunner.addListener(QREvents.START, () => {
this._onRunQueryStart.fire(uri);
this._fireQueryEvent(uri, 'start');
});
info.queryRunner = queryRunner;
info.dataService = this._instantiationService.createInstance(DataService, uri);
this._queryInfoMap.set(uri, info);
return info;
}
public cancelQuery(input: QueryRunner | string): void {
let queryRunner: QueryRunner;
if (typeof input === 'string') {
if (this._queryInfoMap.has(input)) {
queryRunner = this._getQueryInfo(input).queryRunner;
}
} else {
queryRunner = input;
}
if (queryRunner === undefined || !queryRunner.isExecuting) {
// TODO: Cannot cancel query as no query is running.
return;
}
// Switch the spinner to canceling, which will be reset when the query execute sends back its completed event
// TODO indicate on the status bar that the query is being canceled
// Cancel the query
queryRunner.cancelQuery().then(success => undefined, error => {
// On error, show error message and notify that the query is complete so that buttons and other status indicators
// can be correct
this._notificationService.notify({
severity: Severity.Error,
message: strings.format(LocalizedConstants.msgCancelQueryFailed, error)
});
this._fireQueryEvent(queryRunner.uri, 'complete', 0);
});
}
public disposeQuery(ownerUri: string): void {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
queryRunner.disposeQuery();
}
// remove our info map
if (this._queryInfoMap.has(ownerUri)) {
this._queryInfoMap.delete(ownerUri);
}
}
// EDIT DATA METHODS /////////////////////////////////////////////////////
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): void {
// Reuse existing query runner if it exists
let queryRunner: QueryRunner;
let info: QueryInfo;
if (this._queryInfoMap.has(ownerUri)) {
info = this._getQueryInfo(ownerUri);
let existingRunner: QueryRunner = info.queryRunner;
// If the initialization is already in progress
if (existingRunner.isExecuting) {
return;
}
queryRunner = existingRunner;
} else {
info = new QueryInfo();
// We do not have a query runner for this editor, so create a new one
// and map it to the results uri
queryRunner = this._instantiationService.createInstance(QueryRunner, ownerUri);
const resultSetEventType = 'resultSet';
queryRunner.addListener(QREvents.RESULT_SET, resultSet => {
this._fireQueryEvent(ownerUri, resultSetEventType, resultSet);
});
queryRunner.onResultSetUpdate(resultSetSummary => {
this._fireQueryEvent(ownerUri, resultSetEventType, resultSetSummary);
});
queryRunner.addListener(QREvents.BATCH_START, batch => {
let link = undefined;
let messageText = LocalizedConstants.runQueryBatchStartMessage;
if (batch.selection) {
if (info.selectionSnippet) {
// This indicates it's a query string. Do not include line information since it'll be inaccurate, but show some of the
// executed query text
messageText = nls.localize('runQueryStringBatchStartMessage', 'Started executing query "{0}"', info.selectionSnippet);
} else {
link = {
text: strings.format(LocalizedConstants.runQueryBatchStartLine, batch.selection.startLine + 1)
};
}
}
let message = {
message: messageText,
batchId: batch.id,
isError: false,
time: new Date().toLocaleTimeString(),
link: link
};
this._fireQueryEvent(ownerUri, 'message', message);
});
queryRunner.addListener(QREvents.MESSAGE, message => {
this._fireQueryEvent(ownerUri, 'message', message);
});
queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => {
this._onRunQueryComplete.fire(ownerUri);
this._fireQueryEvent(ownerUri, 'complete', totalMilliseconds);
});
queryRunner.addListener(QREvents.START, () => {
this._onRunQueryStart.fire(ownerUri);
this._fireQueryEvent(ownerUri, 'start');
});
queryRunner.addListener(QREvents.EDIT_SESSION_READY, e => {
this._onEditSessionReady.fire(e);
this._fireQueryEvent(e.ownerUri, 'editSessionReady');
});
info.queryRunner = queryRunner;
info.dataService = this._instantiationService.createInstance(DataService, ownerUri);
this._queryInfoMap.set(ownerUri, info);
}
if (queryString) {
if (queryString.length < selectionSnippetMaxLen) {
info.selectionSnippet = queryString;
} else {
info.selectionSnippet = queryString.substring(0, selectionSnippetMaxLen - 3) + '...';
}
}
queryRunner.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString);
}
public cancelInitializeEdit(input: QueryRunner | string): void {
// TODO: Implement query cancellation service
}
public disposeEdit(ownerUri: string): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.disposeEdit(ownerUri);
}
return TPromise.as(null);
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.updateCell(ownerUri, rowId, columnId, newValue).then((result) => result, error => {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('updateCellFailed', 'Update cell failed: ') + error.message
});
return Promise.reject(error);
});
}
return TPromise.as(null);
}
public commitEdit(ownerUri): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.commitEdit(ownerUri).then(() => { }, error => {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('commitEditFailed', 'Commit row failed: ') + error.message
});
return Promise.reject(error);
});
}
return TPromise.as(null);
}
public createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.createRow(ownerUri);
}
return TPromise.as(null);
}
public deleteRow(ownerUri: string, rowId: number): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.deleteRow(ownerUri, rowId);
}
return TPromise.as(null);
}
public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.revertCell(ownerUri, rowId, columnId);
}
return TPromise.as(null);
}
public revertRow(ownerUri: string, rowId: number): Thenable<void> {
// Get existing query runner
let queryRunner = this.internalGetQueryRunner(ownerUri);
if (queryRunner) {
return queryRunner.revertRow(ownerUri, rowId);
}
return TPromise.as(null);
}
public getQueryRunner(ownerUri): QueryRunner {
let queryRunner: QueryRunner = undefined;
if (this._queryInfoMap.has(ownerUri)) {
queryRunner = this._getQueryInfo(ownerUri).queryRunner;
}
// return undefined if not found or is already executing
return queryRunner;
}
// PRIVATE METHODS //////////////////////////////////////////////////////
private internalGetQueryRunner(ownerUri): QueryRunner {
let queryRunner: QueryRunner = undefined;
if (this._queryInfoMap.has(ownerUri)) {
let existingRunner = this._getQueryInfo(ownerUri).queryRunner;
// If the query is not already executing then set it up
if (!existingRunner.isExecuting) {
queryRunner = this._getQueryInfo(ownerUri).queryRunner;
}
}
// return undefined if not found or is already executing
return queryRunner;
}
private _fireGridContentEvent(uri: string, type: string): void {
let info: QueryInfo = this._getQueryInfo(uri);
if (info && info.dataServiceReady) {
let service: DataService = this.getDataService(uri);
if (service) {
// There is no need to queue up these events like there is for the query events because
// if the DataService is not yet ready there will be no grid content to update
service.gridContentObserver.next(type);
}
}
}
private _fireQueryEvent(uri: string, type: string, data?: any) {
let info: QueryInfo = this._getQueryInfo(uri);
if (info.dataServiceReady) {
let service: DataService = this.getDataService(uri);
service.queryEventObserver.next({
type: type,
data: data
});
} else {
let queueItem: QueryEvent = { type: type, data: data };
info.queryEventQueue.push(queueItem);
}
}
private _sendQueuedEvents(uri: string): void {
let info: QueryInfo = this._getQueryInfo(uri);
while (info.queryEventQueue.length > 0) {
let event: QueryEvent = info.queryEventQueue.shift();
this._fireQueryEvent(uri, event.type, event.data);
}
}
public _getQueryInfo(uri: string): QueryInfo {
return this._queryInfoMap.get(uri);
}
// TODO remove this funciton and its usages when #821 in vscode-mssql is fixed and
// the SqlToolsService version is updated in this repo - coquagli 4/19/2017
private _validateSelection(selection: sqlops.ISelectionData): sqlops.ISelectionData {
if (!selection) {
selection = <sqlops.ISelectionData>{};
}
selection.endColumn = selection ? Math.max(0, selection.endColumn) : 0;
selection.endLine = selection ? Math.max(0, selection.endLine) : 0;
selection.startColumn = selection ? Math.max(0, selection.startColumn) : 0;
selection.startLine = selection ? Math.max(0, selection.startLine) : 0;
return selection;
}
}

View File

@@ -0,0 +1,664 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import * as Constants from 'sql/parts/query/common/constants';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
import * as Utils from 'sql/platform/connection/common/utils';
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
import { Deferred } from 'sql/base/common/promise';
import Severity from 'vs/base/common/severity';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import * as types from 'vs/base/common/types';
import { EventEmitter } from 'sql/base/common/eventEmitter';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Emitter, Event } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResultSerializer } from 'sql/platform/node/resultSerializer';
import { TPromise } from 'vs/base/common/winjs.base';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export interface IEditSessionReadyEvent {
ownerUri: string;
success: boolean;
message: string;
}
export const enum EventType {
START = 'start',
COMPLETE = 'complete',
MESSAGE = 'message',
BATCH_START = 'batchStart',
BATCH_COMPLETE = 'batchComplete',
RESULT_SET = 'resultSet',
EDIT_SESSION_READY = 'editSessionReady'
}
export interface IEventType {
start: void;
complete: string;
message: sqlops.IResultMessage;
batchStart: sqlops.BatchSummary;
batchComplete: sqlops.BatchSummary;
resultSet: sqlops.ResultSetSummary;
editSessionReady: IEditSessionReadyEvent;
}
export interface IGridMessage extends sqlops.IResultMessage {
selection: sqlops.ISelectionData;
}
/*
* Query Runner class which handles running a query, reports the results to the content manager,
* and handles getting more rows from the service layer and disposing when the content is closed.
*/
export default class QueryRunner extends Disposable {
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _resultLineOffset: number;
private _totalElapsedMilliseconds: number = 0;
private _isExecuting: boolean = false;
private _hasCompleted: boolean = false;
private _batchSets: sqlops.BatchSummary[] = [];
private _messages: sqlops.IResultMessage[] = [];
private _eventEmitter = new EventEmitter();
private _isQueryPlan: boolean;
public get isQueryPlan(): boolean { return this._isQueryPlan; }
private _planXml = new Deferred<string>();
public get planXml(): Thenable<string> { return this._planXml.promise; }
private _onMessage = this._register(new Emitter<sqlops.IResultMessage>());
public readonly onMessage = this._onMessage.event;
private _onResultSet = this._register(new Emitter<sqlops.ResultSetSummary>());
public readonly onResultSet = this._onResultSet.event;
private _onResultSetUpdate = this._register(new Emitter<sqlops.ResultSetSummary>());
public readonly onResultSetUpdate = this._onResultSetUpdate.event;
private _onQueryStart = this._register(new Emitter<void>());
public readonly onQueryStart: Event<void> = this._onQueryStart.event;
private _onQueryEnd = this._register(new Emitter<string>());
public readonly onQueryEnd: Event<string> = this._onQueryEnd.event;
private _onBatchStart = this._register(new Emitter<sqlops.BatchSummary>());
public readonly onBatchStart: Event<sqlops.BatchSummary> = this._onBatchStart.event;
private _onBatchEnd = this._register(new Emitter<sqlops.BatchSummary>());
public readonly onBatchEnd: Event<sqlops.BatchSummary> = this._onBatchEnd.event;
private _queryStartTime: Date;
public get queryStartTime(): Date {
return this._queryStartTime;
}
private _queryEndTime: Date;
public get queryEndTime(): Date {
return this._queryEndTime;
}
// CONSTRUCTOR /////////////////////////////////////////////////////////
constructor(
public uri: string,
@IQueryManagementService private _queryManagementService: IQueryManagementService,
@INotificationService private _notificationService: INotificationService,
@IConfigurationService private _configurationService: IConfigurationService,
@IClipboardService private _clipboardService: IClipboardService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super();
}
get isExecuting(): boolean {
return this._isExecuting;
}
get hasCompleted(): boolean {
return this._hasCompleted;
}
/**
* For public use only, for private use, directly access the member
*/
public get batchSets(): sqlops.BatchSummary[] {
return this._batchSets.slice(0);
}
/**
* For public use only, for private use, directly access the member
*/
public get messages(): sqlops.IResultMessage[] {
return this._messages.slice(0);
}
// PUBLIC METHODS ======================================================
public addListener<K extends keyof IEventType>(event: K, f: (e: IEventType[K]) => void): IDisposable {
return this._eventEmitter.addListener(event, f);
}
/**
* Cancels the running query, if there is one
*/
public cancelQuery(): Thenable<sqlops.QueryCancelResult> {
return this._queryManagementService.cancelQuery(this.uri);
}
/**
* Runs the query with the provided query
* @param input Query string to execute
*/
public runQuery(input: string, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
/**
* Runs the query by pulling the query from the document using the provided selection data
* @param input selection data
*/
public runQuery(input: sqlops.ISelectionData, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
public runQuery(input, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void> {
return this.doRunQuery(input, false, runOptions);
}
/**
* Runs the current SQL statement by pulling the query from the document using the provided selection data
* @param input selection data
*/
public runQueryStatement(input: sqlops.ISelectionData): Thenable<void> {
return this.doRunQuery(input, true);
}
/**
* Implementation that runs the query with the provided query
* @param input Query string to execute
*/
private doRunQuery(input: string, runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
private doRunQuery(input: sqlops.ISelectionData, runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void>;
private doRunQuery(input, runCurrentStatement: boolean, runOptions?: sqlops.ExecutionPlanOptions): Thenable<void> {
if (this.isExecuting) {
return TPromise.as(undefined);
}
this._planXml = new Deferred<string>();
this._batchSets = [];
this._hasCompleted = false;
this._queryStartTime = undefined;
this._queryEndTime = undefined;
this._messages = [];
if (types.isObject(input) || types.isUndefinedOrNull(input)) {
// Update internal state to show that we're executing the query
this._resultLineOffset = input ? input.startLine : 0;
this._isExecuting = true;
this._totalElapsedMilliseconds = 0;
// TODO issue #228 add statusview callbacks here
if (runOptions && (runOptions.displayActualQueryPlan || runOptions.displayEstimatedQueryPlan)) {
this._isQueryPlan = true;
} else {
this._isQueryPlan = false;
}
// Send the request to execute the query
return runCurrentStatement
? this._queryManagementService.runQueryStatement(this.uri, input.startLine, input.startColumn).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e))
: this._queryManagementService.runQuery(this.uri, input, runOptions).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e));
} else if (types.isString(input)) {
// Update internal state to show that we're executing the query
this._isExecuting = true;
this._totalElapsedMilliseconds = 0;
return this._queryManagementService.runQueryString(this.uri, input).then(() => this.handleSuccessRunQueryResult(), e => this.handleFailureRunQueryResult(e));
} else {
return Promise.reject('Unknown input');
}
}
private handleSuccessRunQueryResult() {
// this isn't exact, but its the best we can do
this._queryStartTime = new Date();
// The query has started, so lets fire up the result pane
this._onQueryStart.fire();
this._eventEmitter.emit(EventType.START);
this._queryManagementService.registerRunner(this, this.uri);
}
private handleFailureRunQueryResult(error: any) {
// Attempting to launch the query failed, show the error message
const eol = this.getEolString();
let message = nls.localize('query.ExecutionFailedError', 'Execution failed due to an unexpected error: {0}\t{1}', eol, error);
this.handleMessage(<sqlops.QueryExecuteMessageParams>{
ownerUri: this.uri,
message: {
isError: true,
message: message
}
});
this.handleQueryComplete(<sqlops.QueryExecuteCompleteNotificationResult>{ ownerUri: this.uri });
}
/**
* Handle a QueryComplete from the service layer
*/
public handleQueryComplete(result: sqlops.QueryExecuteCompleteNotificationResult): void {
// this also isn't exact but its the best we can do
this._queryEndTime = new Date();
// Store the batch sets we got back as a source of "truth"
this._isExecuting = false;
this._hasCompleted = true;
this._batchSets = result.batchSummaries ? result.batchSummaries : [];
this._batchSets.map(batch => {
if (batch.selection) {
batch.selection.startLine = batch.selection.startLine + this._resultLineOffset;
batch.selection.endLine = batch.selection.endLine + this._resultLineOffset;
}
});
let timeStamp = Utils.parseNumAsTimeString(this._totalElapsedMilliseconds);
this._eventEmitter.emit(EventType.COMPLETE, timeStamp);
// We're done with this query so shut down any waiting mechanisms
let message = {
message: nls.localize('query.message.executionTime', 'Total execution time: {0}', timeStamp),
isError: false,
time: undefined
};
this._messages.push(message);
this._onQueryEnd.fire(timeStamp);
this._onMessage.fire(message);
}
/**
* Handle a BatchStart from the service layer
*/
public handleBatchStart(result: sqlops.QueryExecuteBatchNotificationParams): void {
let batch = result.batchSummary;
// Recalculate the start and end lines, relative to the result line offset
if (batch.selection) {
batch.selection.startLine += this._resultLineOffset;
batch.selection.endLine += this._resultLineOffset;
}
// Set the result sets as an empty array so that as result sets complete we can add to the list
batch.resultSetSummaries = [];
// Store the batch
this._batchSets[batch.id] = batch;
let message = {
// account for index by 1
message: nls.localize('query.message.startQuery', 'Started executing query at Line {0}', batch.selection.startLine + 1),
time: new Date(batch.executionStart).toLocaleTimeString(),
selection: batch.selection,
isError: false
};
this._messages.push(message);
this._eventEmitter.emit(EventType.BATCH_START, batch);
this._onMessage.fire(message);
this._onBatchStart.fire(batch);
}
/**
* Handle a BatchComplete from the service layer
*/
public handleBatchComplete(result: sqlops.QueryExecuteBatchNotificationParams): void {
let batch: sqlops.BatchSummary = result.batchSummary;
// Store the batch again to get the rest of the data
this._batchSets[batch.id] = batch;
let executionTime = <number>(Utils.parseTimeString(batch.executionElapsed) || 0);
this._totalElapsedMilliseconds += executionTime;
if (executionTime > 0) {
// send a time message in the format used for query complete
this.sendBatchTimeMessage(batch.id, Utils.parseNumAsTimeString(executionTime));
}
this._eventEmitter.emit(EventType.BATCH_COMPLETE, batch);
this._onBatchEnd.fire(batch);
}
/**
* Handle a ResultSetComplete from the service layer
*/
public handleResultSetAvailable(result: sqlops.QueryExecuteResultSetNotificationParams): void {
if (result && result.resultSetSummary) {
let resultSet = result.resultSetSummary;
let batchSet: sqlops.BatchSummary;
if (!resultSet.batchId) {
// Missing the batchId. In this case, default to always using the first batch in the list
// or create one in the case the DMP extension didn't obey the contract perfectly
if (this._batchSets.length > 0) {
batchSet = this._batchSets[0];
} else {
batchSet = <sqlops.BatchSummary>{
id: 0,
selection: undefined,
hasError: false,
resultSetSummaries: []
};
this._batchSets[0] = batchSet;
}
} else {
batchSet = this._batchSets[resultSet.batchId];
}
// handle getting queryPlanxml if we need too
if (this.isQueryPlan) {
// check if this result has show plan, this needs work, it won't work for any other provider
let hasShowPlan = !!result.resultSetSummary.columnInfo.find(e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan');
if (hasShowPlan) {
this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => this._planXml.resolve(e.resultSubset.rows[0][0].displayValue));
}
}
// we will just ignore the set if we already have it
// ideally this should never happen
if (batchSet && !batchSet.resultSetSummaries[resultSet.id]) {
// Store the result set in the batch and emit that a result set has completed
batchSet.resultSetSummaries[resultSet.id] = resultSet;
this._eventEmitter.emit(EventType.RESULT_SET, resultSet);
this._onResultSet.fire(resultSet);
}
}
}
public handleResultSetUpdated(result: sqlops.QueryExecuteResultSetNotificationParams): void {
if (result && result.resultSetSummary) {
let resultSet = result.resultSetSummary;
let batchSet: sqlops.BatchSummary;
batchSet = this._batchSets[resultSet.batchId];
// handle getting queryPlanxml if we need too
if (this.isQueryPlan) {
// check if this result has show plan, this needs work, it won't work for any other provider
let hasShowPlan = !!result.resultSetSummary.columnInfo.find(e => e.columnName === 'Microsoft SQL Server 2005 XML Showplan');
if (hasShowPlan) {
this.getQueryRows(0, 1, result.resultSetSummary.batchId, result.resultSetSummary.id).then(e => this._planXml.resolve(e.resultSubset.rows[0][0].displayValue));
}
}
if (batchSet) {
// Store the result set in the batch and emit that a result set has completed
batchSet.resultSetSummaries[resultSet.id] = resultSet;
this._onResultSetUpdate.fire(resultSet);
}
}
}
/**
* Handle a Mssage from the service layer
*/
public handleMessage(obj: sqlops.QueryExecuteMessageParams): void {
let message = obj.message;
message.time = new Date(message.time).toLocaleTimeString();
this._messages.push(message);
// Send the message to the results pane
this._eventEmitter.emit(EventType.MESSAGE, message);
this._onMessage.fire(message);
}
/**
* Get more data rows from the current resultSets from the service layer
*/
public getQueryRows(rowStart: number, numberOfRows: number, batchIndex: number, resultSetIndex: number): Thenable<sqlops.QueryExecuteSubsetResult> {
let rowData: sqlops.QueryExecuteSubsetParams = <sqlops.QueryExecuteSubsetParams>{
ownerUri: this.uri,
resultSetIndex: resultSetIndex,
rowsCount: numberOfRows,
rowsStartIndex: rowStart,
batchIndex: batchIndex
};
return this._queryManagementService.getQueryRows(rowData).then(r => r, error => {
// this._notificationService.notify({
// severity: Severity.Error,
// message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
// });
return error;
});
}
/*
* Handle a session ready event for Edit Data
*/
public initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Thenable<void> {
// Update internal state to show that we're executing the query
this._isExecuting = true;
this._totalElapsedMilliseconds = 0;
// TODO issue #228 add statusview callbacks here
return this._queryManagementService.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString).then(result => {
// The query has started, so lets fire up the result pane
this._eventEmitter.emit(EventType.START);
this._queryManagementService.registerRunner(this, ownerUri);
}, error => {
// Attempting to launch the query failed, show the error message
// TODO issue #228 add statusview callbacks here
this._isExecuting = false;
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('query.initEditExecutionFailed', 'Init Edit Execution failed: ') + error
});
});
}
/**
* Retrieves a number of rows from an edit session
* @param rowStart The index of the row to start returning (inclusive)
* @param numberOfRows The number of rows to return
*/
public getEditRows(rowStart: number, numberOfRows: number): Thenable<sqlops.EditSubsetResult> {
const self = this;
let rowData: sqlops.EditSubsetParams = {
ownerUri: this.uri,
rowCount: numberOfRows,
rowStartIndex: rowStart
};
return new Promise<sqlops.EditSubsetResult>((resolve, reject) => {
self._queryManagementService.getEditRows(rowData).then(result => {
if (!result.hasOwnProperty('rowCount')) {
let error = `Nothing returned from subset query`;
self._notificationService.notify({
severity: Severity.Error,
message: error
});
reject(error);
}
resolve(result);
}, error => {
// let errorMessage = nls.localize('query.moreRowsFailedError', 'Something went wrong getting more rows:');
// self._notificationService.notify({
// severity: Severity.Error,
// message: `${errorMessage} ${error}`
// });
reject(error);
});
});
}
public handleEditSessionReady(ownerUri: string, success: boolean, message: string): void {
this._eventEmitter.emit(EventType.EDIT_SESSION_READY, { ownerUri, success, message });
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<sqlops.EditUpdateCellResult> {
return this._queryManagementService.updateCell(ownerUri, rowId, columnId, newValue);
}
public commitEdit(ownerUri): Thenable<void> {
return this._queryManagementService.commitEdit(ownerUri);
}
public createRow(ownerUri: string): Thenable<sqlops.EditCreateRowResult> {
return this._queryManagementService.createRow(ownerUri).then(result => {
return result;
});
}
public deleteRow(ownerUri: string, rowId: number): Thenable<void> {
return this._queryManagementService.deleteRow(ownerUri, rowId);
}
public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<sqlops.EditRevertCellResult> {
return this._queryManagementService.revertCell(ownerUri, rowId, columnId).then(result => {
return result;
});
}
public revertRow(ownerUri: string, rowId: number): Thenable<void> {
return this._queryManagementService.revertRow(ownerUri, rowId);
}
public disposeEdit(ownerUri: string): Thenable<void> {
return this._queryManagementService.disposeEdit(ownerUri);
}
/**
* Disposes the Query from the service client
*/
public disposeQuery(): void {
this._queryManagementService.disposeQuery(this.uri).then(() => {
this.dispose();
});
}
public dispose() {
this._batchSets = undefined;
super.dispose();
}
get totalElapsedMilliseconds(): number {
return this._totalElapsedMilliseconds;
}
/**
* Sends a copy request
* @param selection The selection range to copy
* @param batchId The batch id of the result to copy from
* @param resultId The result id of the result to copy from
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
*/
copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
const self = this;
let copyString = '';
const eol = this.getEolString();
// create a mapping of the ranges to get promises
let tasks = selection.map((range, i) => {
return () => {
return self.getQueryRows(range.fromRow, range.toRow - range.fromRow + 1, batchId, resultId).then((result) => {
// If there was a previous selection separate it with a line break. Currently
// when there are multiple selections they are never on the same line
if (i > 0) {
copyString += eol;
}
if (self.shouldIncludeHeaders(includeHeaders)) {
let columnHeaders = self.getColumnHeaders(batchId, resultId, range);
if (columnHeaders !== undefined) {
copyString += columnHeaders.join('\t') + eol;
}
}
// Iterate over the rows to paste into the copy string
for (let rowIndex: number = 0; rowIndex < result.resultSubset.rows.length; rowIndex++) {
let row = result.resultSubset.rows[rowIndex];
let cellObjects = row.slice(range.fromCell, (range.toCell + 1));
// Remove newlines if requested
let cells = self.shouldRemoveNewLines()
? cellObjects.map(x => self.removeNewLines(x.displayValue))
: cellObjects.map(x => x.displayValue);
copyString += cells.join('\t');
if (rowIndex < result.resultSubset.rows.length - 1) {
copyString += eol;
}
}
});
};
});
if (tasks.length > 0) {
let p = tasks[0]();
for (let i = 1; i < tasks.length; i++) {
p = p.then(tasks[i]);
}
p.then(() => {
this._clipboardService.writeText(copyString);
});
}
}
private getEolString(): string {
const { eol } = this._configurationService.getValue<{ eol: string }>('files');
return eol;
}
private shouldIncludeHeaders(includeHeaders: boolean): boolean {
if (includeHeaders !== undefined) {
// Respect the value explicity passed into the method
return includeHeaders;
}
// else get config option from vscode config
includeHeaders = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.copyIncludeHeaders);
return !!includeHeaders;
}
private shouldRemoveNewLines(): boolean {
// get config copyRemoveNewLine option from vscode config
let removeNewLines: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configCopyRemoveNewLine);
return !!removeNewLines;
}
private getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
let headers: string[] = undefined;
let batchSummary: sqlops.BatchSummary = this._batchSets[batchId];
if (batchSummary !== undefined) {
let resultSetSummary = batchSummary.resultSetSummaries[resultId];
headers = resultSetSummary.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => {
return info.columnName;
});
}
return headers;
}
private removeNewLines(inputString: string): string {
// This regex removes all newlines in all OS types
// Windows(CRLF): \r\n
// Linux(LF)/Modern MacOS: \n
// Old MacOs: \r
if (types.isUndefinedOrNull(inputString)) {
return 'null';
}
let outputString: string = inputString.replace(/(\r\n|\n|\r)/gm, '');
return outputString;
}
private sendBatchTimeMessage(batchId: number, executionTime: string): void {
// get config copyRemoveNewLine option from vscode config
let showBatchTime: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configShowBatchTime);
if (showBatchTime) {
let message: sqlops.IResultMessage = {
batchId: batchId,
message: nls.localize('elapsedBatchTime', 'Batch execution time: {0}', executionTime),
time: undefined,
isError: false
};
this._messages.push(message);
// Send the message to the results pane
this._onMessage.fire(message);
}
}
public serializeResults(batchId: number, resultSetId: number, format: SaveFormat, selection: Slick.Range[]) {
return this.instantiationService.createInstance(ResultSerializer).saveResults(this.uri, { selection, format, batchIndex: batchId, resultSetNumber: resultSetId });
}
}

View File

@@ -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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
export class MssqlRestoreInfo implements sqlops.RestoreInfo {
options: { [name: string]: any };
taskExecutionMode: sqlops.TaskExecutionMode;
public constructor() {
this.options = {};
}
public get sessionId(): string {
return this.options['sessionId'];
}
public set sessionId(value: string) {
this.options['sessionId'] = value;
}
public get backupFilePaths(): string {
return this.options['backupFilePaths'];
}
public set backupFilePaths(value: string) {
this.options['backupFilePaths'] = value;
}
public get targetDatabaseName(): string {
return this.options['targetDatabaseName'];
}
public set targetDatabaseName(value: string) {
this.options['targetDatabaseName'] = value;
}
public get sourceDatabaseName(): string {
return this.options['sourceDatabaseName'];
}
public set sourceDatabaseName(value: string) {
this.options['sourceDatabaseName'] = value;
}
public get relocateDbFiles(): boolean {
return this.options['relocateDbFiles'];
}
public set relocateDbFiles(value: boolean) {
this.options['relocateDbFiles'] = value;
}
public get dataFileFolder(): string {
return this.options['dataFileFolder'];
}
public set dataFileFolder(value: string) {
this.options['dataFileFolder'] = value;
}
public get logFileFolder(): string {
return this.options['logFileFolder'];
}
public set logFileFolder(value: string) {
this.options['logFileFolder'] = value;
}
public get selectedBackupSets(): string[] {
return this.options['selectedBackupSets'];
}
public set selectedBackupSets(value: string[]) {
this.options['selectedBackupSets'] = value;
}
public get readHeaderFromMedia(): boolean {
return this.options['readHeaderFromMedia'];
}
public set readHeaderFromMedia(value: boolean) {
this.options['readHeaderFromMedia'] = value;
}
public get overwriteTargetDatabase(): boolean {
return this.options['overwriteTargetDatabase'];
}
public set overwriteTargetDatabase(value: boolean) {
this.options['overwriteTargetDatabase'] = value;
}
}

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import * as sqlops from 'sqlops';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
export { TaskExecutionMode } from 'sql/platform/backup/common/backupService';
export const SERVICE_ID = 'restoreService';
export const IRestoreService = createDecorator<IRestoreService>(SERVICE_ID);
export interface IRestoreService {
_serviceBrand: any;
/**
* Register a disaster recovery provider
*/
registerProvider(providerId: string, provider: sqlops.RestoreProvider): void;
/**
* Restore a data source using a backup file or database
*/
restore(connectionUri: string, restoreInfo: sqlops.RestoreInfo): Thenable<sqlops.RestoreResponse>;
/**
* Gets restore plan to do the restore operation on a database
*/
getRestorePlan(connectionUri: string, restoreInfo: sqlops.RestoreInfo): Thenable<sqlops.RestorePlanResponse>;
/**
* Gets restore config Info
*/
getRestoreConfigInfo(connectionUri: string): Thenable<sqlops.RestoreConfigInfo>;
/**
* Cancel restore plan
*/
cancelRestorePlan(connectionUri: string, restoreInfo: sqlops.RestoreInfo): Thenable<boolean>;
}
export const IRestoreDialogController = createDecorator<IRestoreDialogController>('restoreDialogService');
export interface IRestoreDialogController {
_serviceBrand: any;
showDialog(connection: IConnectionProfile): TPromise<void>;
}

View File

@@ -0,0 +1,359 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as types from 'vs/base/common/types';
import * as sqlops from 'sqlops';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { IRestoreService, IRestoreDialogController, TaskExecutionMode } from 'sql/platform/restore/common/restoreService';
import { OptionsDialog } from 'sql/base/browser/ui/modal/optionsDialog';
import { RestoreDialog } from 'sql/parts/disasterRecovery/restore/restoreDialog';
import * as ConnectionConstants from 'sql/platform/connection/common/constants';
import { MssqlRestoreInfo } from 'sql/platform/restore/common/mssqlRestoreInfo';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { ProviderConnectionInfo } from 'sql/platform/connection/common/providerConnectionInfo';
import * as Utils from 'sql/platform/connection/common/utils';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { ITaskService } from 'sql/platform/taskHistory/common/taskService';
import { TaskStatus, TaskNode } from 'sql/parts/taskHistory/common/taskNode';
import * as Constants from 'sql/common/constants';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
export class RestoreService implements IRestoreService {
public _serviceBrand: any;
private _providers: { [handle: string]: sqlops.RestoreProvider; } = Object.create(null);
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
}
/**
* Gets restore config Info
*/
getRestoreConfigInfo(connectionUri: string): Thenable<sqlops.RestoreConfigInfo> {
return new Promise<sqlops.RestoreConfigInfo>((resolve, reject) => {
let providerResult = this.getProvider(connectionUri);
if (providerResult) {
providerResult.provider.getRestoreConfigInfo(connectionUri).then(result => {
resolve(result);
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
/**
* Restore a data source using a backup file or database
*/
restore(connectionUri: string, restoreInfo: sqlops.RestoreInfo): Thenable<sqlops.RestoreResponse> {
return new Promise<sqlops.RestoreResponse>((resolve, reject) => {
let providerResult = this.getProvider(connectionUri);
if (providerResult) {
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.RestoreRequested, { provider: providerResult.providerName });
providerResult.provider.restore(connectionUri, restoreInfo).then(result => {
resolve(result);
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
private getProvider(connectionUri: string): { provider: sqlops.RestoreProvider, providerName: string } {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
return { provider: this._providers[providerId], providerName: providerId };
} else {
return undefined;
}
}
/**
* Gets restore plan to do the restore operation on a database
*/
getRestorePlan(connectionUri: string, restoreInfo: sqlops.RestoreInfo): Thenable<sqlops.RestorePlanResponse> {
return new Promise<sqlops.RestorePlanResponse>((resolve, reject) => {
let providerResult = this.getProvider(connectionUri);
if (providerResult) {
providerResult.provider.getRestorePlan(connectionUri, restoreInfo).then(result => {
resolve(result);
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
/**
* Cancels a restore plan
*/
cancelRestorePlan(connectionUri: string, restoreInfo: sqlops.RestoreInfo): Thenable<boolean> {
return new Promise<boolean>((resolve, reject) => {
let providerResult = this.getProvider(connectionUri);
if (providerResult) {
providerResult.provider.cancelRestorePlan(connectionUri, restoreInfo).then(result => {
resolve(result);
}, error => {
reject(error);
});
} else {
reject(Constants.InvalidProvider);
}
});
}
/**
* Register a disaster recovery provider
*/
public registerProvider(providerId: string, provider: sqlops.RestoreProvider): void {
this._providers[providerId] = provider;
}
}
export class RestoreDialogController implements IRestoreDialogController {
_serviceBrand: any;
private _restoreDialogs: { [provider: string]: RestoreDialog | OptionsDialog } = {};
private _currentProvider: string;
private _ownerUri: string;
private _sessionId: string;
private readonly _restoreFeature = 'Restore';
private readonly _restoreTaskName: string = 'Restore Database';
private readonly _restoreCompleted: string = 'Completed';
private _optionValues: { [optionName: string]: any } = {};
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IRestoreService private _restoreService: IRestoreService,
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@ITaskService private _taskService: ITaskService
) {
}
private handleOnRestore(isScriptOnly: boolean = false): void {
let restoreOption = this.setRestoreOption();
if (isScriptOnly) {
restoreOption.taskExecutionMode = TaskExecutionMode.script;
} else {
restoreOption.taskExecutionMode = TaskExecutionMode.executeAndScript;
}
this._restoreService.restore(this._ownerUri, restoreOption).then(result => {
const self = this;
let connectionProfile = self._connectionService.getConnectionProfile(self._ownerUri);
let activeNode = self._objectExplorerService.getObjectExplorerNode(connectionProfile);
this._taskService.onTaskComplete(response => {
if (result.taskId === response.id && this.isSuccessfulRestore(response) && activeNode) {
self._objectExplorerService.refreshTreeNode(activeNode.getSession(), activeNode).then(result => {
self._objectExplorerService.getServerTreeView().refreshTree();
});
}
});
let restoreDialog = this._restoreDialogs[this._currentProvider];
restoreDialog.close();
});
}
private isSuccessfulRestore(response: TaskNode): boolean {
return (response.taskName === this._restoreTaskName &&
response.message === this._restoreCompleted &&
(response.status === TaskStatus.Succeeded ||
response.status === TaskStatus.SucceededWithWarning) &&
(response.taskExecutionMode === TaskExecutionMode.execute ||
response.taskExecutionMode === TaskExecutionMode.executeAndScript));
}
private handleMssqlOnValidateFile(overwriteTargetDatabase: boolean = false): void {
let restoreDialog = this._restoreDialogs[this._currentProvider] as RestoreDialog;
this._restoreService.getRestorePlan(this._ownerUri, this.setRestoreOption(overwriteTargetDatabase)).then(restorePlanResponse => {
this._sessionId = restorePlanResponse.sessionId;
if (restorePlanResponse.errorMessage) {
restoreDialog.onValidateResponseFail(restorePlanResponse.errorMessage);
} else {
restoreDialog.removeErrorMessage();
restoreDialog.viewModel.onRestorePlanResponse(restorePlanResponse);
}
if (restorePlanResponse.canRestore && !this.isEmptyBackupset()) {
restoreDialog.enableRestoreButton(true);
} else {
restoreDialog.enableRestoreButton(false);
}
}, error => {
restoreDialog.showError(error);
});
}
/**
* Temporary fix for bug #2506: Restore button not disabled when there's not backup set to restore
* Will remove this function once there is a fix in the service (bug #2572)
*/
private isEmptyBackupset(): boolean {
let restoreDialog = this._restoreDialogs[this._currentProvider] as RestoreDialog;
if (!types.isUndefinedOrNull(restoreDialog.viewModel.selectedBackupSets) && restoreDialog.viewModel.selectedBackupSets.length === 0) {
return true;
}
return false;
}
private getMssqlRestoreConfigInfo(): Promise<void> {
return new Promise<void>((resolve, reject) => {
let restoreDialog = this._restoreDialogs[this._currentProvider] as RestoreDialog;
this._restoreService.getRestoreConfigInfo(this._ownerUri).then(restoreConfigInfo => {
restoreDialog.viewModel.updateOptionWithConfigInfo(restoreConfigInfo.configInfo);
resolve();
}, error => {
restoreDialog.showError(error);
reject(error);
});
});
}
private setRestoreOption(overwriteTargetDatabase: boolean = false): sqlops.RestoreInfo {
let restoreInfo = undefined;
let providerId: string = this.getCurrentProviderId();
if (providerId === ConnectionConstants.mssqlProviderName) {
restoreInfo = new MssqlRestoreInfo();
if (this._sessionId) {
restoreInfo.sessionId = this._sessionId;
}
let restoreDialog = this._restoreDialogs[providerId] as RestoreDialog;
restoreInfo.backupFilePaths = restoreDialog.viewModel.filePath;
restoreInfo.readHeaderFromMedia = restoreDialog.viewModel.readHeaderFromMedia;
restoreInfo.selectedBackupSets = restoreDialog.viewModel.selectedBackupSets;
restoreInfo.sourceDatabaseName = restoreDialog.viewModel.sourceDatabaseName;
if (restoreDialog.viewModel.targetDatabaseName) {
restoreInfo.targetDatabaseName = restoreDialog.viewModel.targetDatabaseName;
}
restoreInfo.overwriteTargetDatabase = overwriteTargetDatabase;
// Set other restore options
restoreDialog.viewModel.getRestoreAdvancedOptions(restoreInfo.options);
} else {
restoreInfo = { options: this._optionValues };
}
return restoreInfo;
}
private getRestoreOption(): sqlops.ServiceOption[] {
let options: sqlops.ServiceOption[] = [];
let providerId: string = this.getCurrentProviderId();
let providerCapabilities = this._capabilitiesService.getLegacyCapabilities(providerId);
if (providerCapabilities) {
let restoreMetadataProvider = providerCapabilities.features.find(f => f.featureName === this._restoreFeature);
if (restoreMetadataProvider) {
options = restoreMetadataProvider.optionsMetadata;
}
}
return options;
}
private handleOnClose(): void {
this._connectionService.disconnect(this._ownerUri);
}
private handleOnCancel(): void {
let restoreInfo = new MssqlRestoreInfo();
restoreInfo.sessionId = this._sessionId;
this._restoreService.cancelRestorePlan(this._ownerUri, restoreInfo).then(() => {
this._connectionService.disconnect(this._ownerUri);
});
}
public showDialog(connection: IConnectionProfile): TPromise<void> {
return new TPromise<void>((resolve, reject) => {
let result: void;
this._ownerUri = this._connectionService.getConnectionUri(connection)
+ ProviderConnectionInfo.idSeparator
+ Utils.ConnectionUriRestoreIdAttributeName
+ ProviderConnectionInfo.nameValueSeparator
+ '0';
if (!this._connectionService.isConnected(this._ownerUri)) {
this._connectionService.connect(connection, this._ownerUri).then(connectionResult => {
this._sessionId = null;
this._currentProvider = this.getCurrentProviderId();
if (!this._restoreDialogs[this._currentProvider]) {
let newRestoreDialog: RestoreDialog | OptionsDialog = undefined;
if (this._currentProvider === ConnectionConstants.mssqlProviderName) {
let provider = this._currentProvider;
newRestoreDialog = this._instantiationService.createInstance(RestoreDialog, this.getRestoreOption());
newRestoreDialog.onCancel(() => this.handleOnCancel());
newRestoreDialog.onRestore((isScriptOnly) => this.handleOnRestore(isScriptOnly));
newRestoreDialog.onValidate((overwriteTargetDatabase) => this.handleMssqlOnValidateFile(overwriteTargetDatabase));
newRestoreDialog.onDatabaseListFocused(() => this.fetchDatabases(provider));
} else {
newRestoreDialog = this._instantiationService.createInstance(
OptionsDialog, 'Restore database - ' + connection.serverName + ':' + connection.databaseName, 'RestoreOptions', undefined);
newRestoreDialog.onOk(() => this.handleOnRestore());
}
newRestoreDialog.onCloseEvent(() => this.handleOnClose());
newRestoreDialog.render();
this._restoreDialogs[this._currentProvider] = newRestoreDialog;
}
if (this._currentProvider === ConnectionConstants.mssqlProviderName) {
let restoreDialog = this._restoreDialogs[this._currentProvider] as RestoreDialog;
restoreDialog.viewModel.resetRestoreOptions(connection.databaseName);
this.getMssqlRestoreConfigInfo().then(() => {
restoreDialog.open(connection.serverName, this._ownerUri);
restoreDialog.validateRestore();
}, restoreConfigError => {
reject(restoreConfigError);
});
} else {
let restoreDialog = this._restoreDialogs[this._currentProvider] as OptionsDialog;
restoreDialog.open(this.getRestoreOption(), this._optionValues);
}
resolve(result);
}, error => {
reject(error);
});
}
});
}
private getCurrentProviderId(): string {
return this._connectionService.getProviderIdFromUri(this._ownerUri);
}
private fetchDatabases(provider: string): void {
this._connectionService.listDatabases(this._ownerUri).then(result => {
if (result && result.databaseNames) {
(<RestoreDialog>this._restoreDialogs[provider]).databaseListOptions = result.databaseNames;
}
});
}
}

View File

@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* 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 { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ScriptOperation } from 'sql/workbench/common/taskUtilities';
import * as sqlops from 'sqlops';
import { error } from 'sql/base/common/log';
export const SERVICE_ID = 'scriptingService';
export const IScriptingService = createDecorator<IScriptingService>(SERVICE_ID);
export interface IScriptingService {
_serviceBrand: any;
script(connectionUri: string, metadata: sqlops.ObjectMetadata, operation: ScriptOperation, paramDetails: sqlops.ScriptingParamDetails): Thenable<sqlops.ScriptingResult>;
/**
* Register a scripting provider
*/
registerProvider(providerId: string, provider: sqlops.ScriptingProvider): void;
/**
* Specifies whether a provider with a given ID has been registered or not
*/
isProviderRegistered(providerId: string): boolean;
/**
* Callback method for when scripting is complete
*/
onScriptingComplete(handle: number, scriptingCompleteResult: sqlops.ScriptingCompleteResult): void;
/**
* Returns the result for an operation if the operation failed
*/
getOperationFailedResult(operationId: string): sqlops.ScriptingCompleteResult;
}
export class ScriptingService implements IScriptingService {
public _serviceBrand: any;
private disposables: IDisposable[] = [];
private _providers: { [handle: string]: sqlops.ScriptingProvider; } = Object.create(null);
private failedScriptingOperations: { [operationId: string]: sqlops.ScriptingCompleteResult } = {};
constructor( @IConnectionManagementService private _connectionService: IConnectionManagementService) { }
/**
* Call the service for scripting based on provider and scripting operation
* @param connectionUri
* @param metadata
* @param operation
* @param paramDetails
*/
public script(connectionUri: string, metadata: sqlops.ObjectMetadata, operation: ScriptOperation, paramDetails: sqlops.ScriptingParamDetails): Thenable<sqlops.ScriptingResult> {
let providerId: string = this._connectionService.getProviderIdFromUri(connectionUri);
if (providerId) {
let provider = this._providers[providerId];
if (provider) {
return provider.scriptAsOperation(connectionUri, operation, metadata, paramDetails);
}
}
return Promise.resolve(undefined);
}
/**
* Callback method for when scripting is complete
* @param handle
* @param scriptingCompleteResult
*/
public onScriptingComplete(handle: number, scriptingCompleteResult: sqlops.ScriptingCompleteResult): void {
if (scriptingCompleteResult && scriptingCompleteResult.hasError && scriptingCompleteResult.errorMessage) {
error(`Scripting failed. error: ${scriptingCompleteResult.errorMessage}`);
if (scriptingCompleteResult.operationId) {
this.failedScriptingOperations[scriptingCompleteResult.operationId] = scriptingCompleteResult;
}
}
}
/**
* Returns the result for an operation if the operation failed
* @param operationId Operation Id
*/
public getOperationFailedResult(operationId: string): sqlops.ScriptingCompleteResult {
if (operationId && operationId in this.failedScriptingOperations) {
return this.failedScriptingOperations[operationId];
} else {
return undefined;
}
}
/**
* Register a scripting provider
*/
public registerProvider(providerId: string, provider: sqlops.ScriptingProvider): void {
this._providers[providerId] = provider;
}
public isProviderRegistered(providerId: string): boolean {
let provider = this._providers[providerId];
return !!provider;
}
public dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import * as sqlops from 'sqlops';
export const SERVICE_ID = 'serializationService';
export interface SerializationProviderEvents {
onSaveAs(saveFormat: string, savePath: string, results: string, appendToFile: boolean): Thenable<sqlops.SaveResultRequestResult>;
}
export const ISerializationService = createDecorator<ISerializationService>(SERVICE_ID);
export interface ISerializationService {
_serviceBrand: any;
saveAs(saveFormat: string, savePath: string, results: string, appendToFile: boolean): Thenable<sqlops.SaveResultRequestResult>;
disabledSaveAs(): Thenable<sqlops.SaveResultRequestResult>;
addEventListener(handle: number, events: SerializationProviderEvents): IDisposable;
getSerializationFeatureMetadataProvider(ownerUri: string): sqlops.FeatureMetadataProvider;
}
export class SerializationService implements ISerializationService {
_serviceBrand: any;
private disposables: IDisposable[] = [];
private _serverEvents: { [handle: number]: SerializationProviderEvents; } = Object.create(null);
private _lastHandle: number;
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService
) {
}
public addEventListener(handle: number, events: SerializationProviderEvents): IDisposable {
this._lastHandle = handle;
this._serverEvents[handle] = events;
return {
dispose: () => {
}
};
}
public saveAs(saveFormat: string, savePath: string, results: string, appendToFile: boolean): Thenable<sqlops.SaveResultRequestResult> {
if (this._serverEvents === undefined || this._serverEvents[this._lastHandle] === undefined) {
return this.disabledSaveAs();
}
return this._serverEvents[this._lastHandle].onSaveAs(saveFormat, savePath, results, appendToFile);
}
public disabledSaveAs(): Thenable<sqlops.SaveResultRequestResult> {
return Promise.resolve({ messages: 'Saving results into different format disabled for this data provider.' });
}
public getSerializationFeatureMetadataProvider(ownerUri: string): sqlops.FeatureMetadataProvider {
let providerId: string = this._connectionService.getProviderIdFromUri(ownerUri);
let providerCapabilities = this._capabilitiesService.getLegacyCapabilities(providerId);
if (providerCapabilities) {
return providerCapabilities.features.find(f => f.featureName === SERVICE_ID);
}
return undefined;
}
public dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
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>;
}

View File

@@ -0,0 +1,253 @@
/*---------------------------------------------------------------------------------------------
* 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 sqlops from 'sqlops';
import { TaskNode, TaskStatus, TaskExecutionMode } from 'sql/parts/taskHistory/common/taskNode';
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { localize } from 'vs/nls';
import Severity from 'vs/base/common/severity';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
export const SERVICE_ID = 'taskHistoryService';
export const ITaskService = createDecorator<ITaskService>(SERVICE_ID);
export interface ITaskService {
_serviceBrand: any;
onTaskComplete: Event<TaskNode>;
onAddNewTask: Event<TaskNode>;
handleNewTask(task: TaskNode): void;
handleTaskComplete(eventArgs: TaskStatusChangeArgs): void;
getAllTasks(): TaskNode;
getNumberOfInProgressTasks(): number;
onNewTaskCreated(handle: number, taskInfo: sqlops.TaskInfo);
createNewTask(taskInfo: sqlops.TaskInfo);
updateTask(taskProgressInfo: sqlops.TaskProgressInfo);
onTaskStatusChanged(handle: number, taskProgressInfo: sqlops.TaskProgressInfo);
cancelTask(providerId: string, taskId: string): Thenable<boolean>;
/**
* Register a ObjectExplorer provider
*/
registerProvider(providerId: string, provider: sqlops.TaskServicesProvider): void;
}
export interface TaskStatusChangeArgs {
taskId: string;
status: sqlops.TaskStatus;
message?: string;
script?: string;
}
export class TaskService implements ITaskService {
public _serviceBrand: any;
private _taskQueue: TaskNode;
private _onTaskComplete = new Emitter<TaskNode>();
private _onAddNewTask = new Emitter<TaskNode>();
private _providers: { [handle: string]: sqlops.TaskServicesProvider; } = Object.create(null);
constructor(
@ILifecycleService lifecycleService: ILifecycleService,
@IDialogService private dialogService: IDialogService,
@IQueryEditorService private queryEditorService: IQueryEditorService,
@IConnectionManagementService private connectionManagementService: IConnectionManagementService
) {
this._taskQueue = new TaskNode('Root', undefined, undefined);
this._onTaskComplete = new Emitter<TaskNode>();
this._onAddNewTask = new Emitter<TaskNode>();
lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown()));
}
/**
* Register a ObjectExplorer provider
*/
public registerProvider(providerId: string, provider: sqlops.TaskServicesProvider): void {
this._providers[providerId] = provider;
}
public onNewTaskCreated(handle: number, taskInfo: sqlops.TaskInfo) {
this.createNewTask(taskInfo);
}
public createNewTask(taskInfo: sqlops.TaskInfo) {
let databaseName: string = taskInfo.databaseName;
let serverName: string = taskInfo.serverName;
if (taskInfo && taskInfo.connection) {
let connectionProfile = this.connectionManagementService.getConnectionProfile(taskInfo.connection.connectionId);
if (connectionProfile && !!databaseName) {
databaseName = connectionProfile.databaseName;
}
if (connectionProfile && !!serverName) {
serverName = connectionProfile.serverName;
}
}
let node: TaskNode = new TaskNode(taskInfo.name, serverName, databaseName, taskInfo.taskId, taskInfo.taskExecutionMode, taskInfo.isCancelable);
node.providerName = taskInfo.providerName;
this.handleNewTask(node);
}
public updateTask(taskProgressInfo: sqlops.TaskProgressInfo) {
this.handleTaskComplete({
taskId: taskProgressInfo.taskId,
status: taskProgressInfo.status,
message: taskProgressInfo.message,
script: taskProgressInfo.script
});
}
public onTaskStatusChanged(handle: number, taskProgressInfo: sqlops.TaskProgressInfo) {
this.updateTask(taskProgressInfo);
}
public cancelTask(providerId: string, taskId: string): Thenable<boolean> {
let task = this.getTaskInQueue(taskId);
task.status = TaskStatus.Canceling;
this._onTaskComplete.fire(task);
if (providerId) {
let provider = this._providers[providerId];
if (provider && provider.cancelTask) {
return provider.cancelTask({
taskId: taskId
});
}
} else {
return Promise.resolve(true);
}
return Promise.resolve(undefined);
}
private cancelAllTasks(): Thenable<void> {
return new TPromise<void>((resolve, reject) => {
let promises = this._taskQueue.children.map(task => {
if (task.status === TaskStatus.InProgress || task.status === TaskStatus.NotStarted) {
return this.cancelTask(task.providerName, task.id);
}
return Promise.resolve(true);
});
Promise.all(promises).then(result => {
resolve(undefined);
}).catch(error => {
reject(error);
});
});
}
public handleNewTask(task: TaskNode): void {
if (this._taskQueue.hasChildren) {
this._taskQueue.children.unshift(task);
} else {
this._taskQueue.hasChildren = true;
this._taskQueue.children = [task];
}
this._onAddNewTask.fire(task);
}
public beforeShutdown(): TPromise<boolean> {
const message = localize('InProgressWarning', '1 or more tasks are in progress. Are you sure you want to quit?');
const options = [
localize('taskService.yes', "Yes"),
localize('taskService.no', "No")
];
return new TPromise<boolean>((resolve, reject) => {
let numOfInprogressTasks = this.getNumberOfInProgressTasks();
if (numOfInprogressTasks > 0) {
this.dialogService.show(Severity.Warning, message, options).done(choice => {
switch (choice) {
case 0:
let timeoutId: number;
let isTimeout = false;
this.cancelAllTasks().then(() => {
clearTimeout(timeoutId);
if (!isTimeout) {
resolve(false);
}
}, error => {
clearTimeout(timeoutId);
if (!isTimeout) {
resolve(false);
}
});
timeoutId = setTimeout(function () {
isTimeout = true;
resolve(false);
}, 2000);
break;
case 1:
resolve(true);
}
});
} else {
resolve(false);
}
});
}
public handleTaskComplete(eventArgs: TaskStatusChangeArgs): void {
var task = this.getTaskInQueue(eventArgs.taskId);
if (task) {
task.status = eventArgs.status;
if (eventArgs.message) {
task.message = eventArgs.message;
}
switch (task.status) {
case TaskStatus.Canceled:
case TaskStatus.Succeeded:
case TaskStatus.SucceededWithWarning:
case TaskStatus.Failed:
task.endTime = new Date().toLocaleTimeString();
task.timer.stop();
this._onTaskComplete.fire(task);
break;
default:
break;
}
if ((task.status === TaskStatus.Succeeded || task.status === TaskStatus.SucceededWithWarning)
&& eventArgs.script && eventArgs.script !== '') {
if (task.taskExecutionMode === TaskExecutionMode.script) {
this.queryEditorService.newSqlEditor(eventArgs.script);
} else if (task.taskExecutionMode === TaskExecutionMode.executeAndScript) {
task.script = eventArgs.script;
}
}
}
}
private getTaskInQueue(taskId: string): TaskNode {
if (this._taskQueue.hasChildren) {
return this._taskQueue.children.find(x => x.id === taskId);
}
return undefined;
}
public get onTaskComplete(): Event<TaskNode> {
return this._onTaskComplete.event;
}
public get onAddNewTask(): Event<TaskNode> {
return this._onAddNewTask.event;
}
public getNumberOfInProgressTasks(): number {
if (this._taskQueue.hasChildren) {
var inProgressTasks = this._taskQueue.children.filter(x => x.status === TaskStatus.InProgress);
return inProgressTasks ? inProgressTasks.length : 0;
}
return 0;
}
public getAllTasks(): TaskNode {
return this._taskQueue;
}
}

View File

@@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import * as types from 'vs/base/common/types';
import { TPromise } from 'vs/base/common/winjs.base';

View File

@@ -19,10 +19,10 @@ export class FileTelemetryService implements ITelemetryService {
}
publicLog(eventName: string, data?: ITelemetryData) {
let telemetryData = JSON.stringify(Object.assign({eventName: eventName, data: data}));
let telemetryData = JSON.stringify(Object.assign({ eventName: eventName, data: data }));
if (this._outputFile) {
if (this._isFirst) {
fs.open(this._outputFile, fs.O_WRONLY | fs.O_CREAT, (err, fr) => {
fs.open(this._outputFile, fs.O_WRONLY | fs.O_CREAT, (err, fr) => {
fs.writeFileSync(this._outputFile, telemetryData + '\n');
this._isFirst = false;
});
@@ -40,4 +40,4 @@ export class FileTelemetryService implements ITelemetryService {
machineId: 'someValue.machineId'
});
}
}
}