Compare commits

...

12 Commits
1.3.3 ... 1.3.4

Author SHA1 Message Date
Karl Burtram
59b2e706ca Bump Import extension to 0.4.2 2018-11-27 17:59:34 -08:00
Karl Burtram
8bf835c531 Update SQL Tools Service to 1.5.0-alpha.59 2018-11-27 16:37:46 -08:00
Matt Irvine
087ed7c132 Make resource parameter optional for getSecurityToken API (#3322) 2018-11-27 16:12:30 -08:00
kisantia
4c075df327 DacFx import/export wizard (#3162)
Basic wizard to import/export bacpacs and deploy/extract dacpacs
2018-11-27 16:10:17 -08:00
Yurong He
9ea8baca05 Hide run button in markdown editor and pull toggleMoreAction to a seperate class (#3321) 2018-11-27 15:32:20 -08:00
Karl Burtram
9b6784720e Add EXCEPT,INTERSECT and DATALENGTH to color syntax (#3320) 2018-11-27 13:52:07 -08:00
Karl Burtram
3761e1dd60 Port event-stream changes from vscode (#3317) 2018-11-27 12:55:44 -08:00
Yurong He
b3eb809550 Hide ToggleMoreAction for inactive cell (#3235)
* Hide ToggleMoreAction for inactive cell

* Revert wrong merge

* Undo bad merge

* Enable markdown to hide ToggleMoreAction

* Resolve PR comments

* Fixed the name

* Change name

* Fix toggleMoreActions by passing in CellContext instead of just nb model

* Fixed the warning by removing notificationService from the caller
2018-11-27 12:54:27 -08:00
Matt Irvine
cb72865dcc Enable Azure Active Directory MFA authentication (#3125) 2018-11-27 11:13:47 -08:00
Anthony Dresser
d646b4729b Revert "Result Streaming (#3124)" (#3312)
This reverts commit 8925d44807.
2018-11-27 10:42:41 -08:00
Karl Burtram
a2dd903d0d Bump import, profiler, and agent extensions 2018-11-27 07:25:31 -08:00
Karl Burtram
28ed378ee7 Bump Azure Data Studio to 1.3.4 2018-11-27 05:53:43 -08:00
82 changed files with 3863 additions and 2616 deletions

View File

@@ -9,21 +9,20 @@
"@types/mime": "0.0.29",
"@types/minimatch": "^3.0.3",
"@types/node": "8.0.33",
"@types/request": "^2.47.0",
"@types/xml2js": "0.0.33",
"@types/request": "^2.47.0",
"azure-storage": "^2.1.0",
"decompress": "^4.2.0",
"del": "^3.0.0",
"documentdb": "1.13.0",
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
"fs-extra-promise": "^1.0.1",
"github-releases": "^0.4.1",
"mime": "^1.3.4",
"minimist": "^1.2.0",
"request": "^2.85.0",
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
"typescript": "2.9.2",
"vscode": "^1.0.1",
"xml2js": "^0.4.17"
"xml2js": "^0.4.17",
"github-releases": "^0.4.1",
"request": "^2.85.0"
},
"scripts": {
"compile": "tsc -p tsconfig.build.json",
@@ -31,4 +30,4 @@
"postinstall": "npm run compile",
"npmCheckJs": "tsc --noEmit"
}
}
}

View File

@@ -571,18 +571,6 @@ deep-assign@^1.0.0:
dependencies:
is-obj "^1.0.0"
del@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=
dependencies:
globby "^6.1.0"
is-path-cwd "^1.0.0"
is-path-in-cwd "^1.0.0"
p-map "^1.1.1"
pify "^3.0.0"
rimraf "^2.2.8"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -954,29 +942,6 @@ glob@^5.0.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
globby@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
dependencies:
array-union "^1.0.1"
glob "^7.0.3"
object-assign "^4.0.1"
pify "^2.0.0"
pinkie-promise "^2.0.0"
glogg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.1.tgz#dcf758e44789cc3f3d32c1f3562a3676e6a34810"
@@ -1348,25 +1313,6 @@ is-obj@^1.0.0:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
is-path-cwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=
is-path-in-cwd@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52"
integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==
dependencies:
is-path-inside "^1.0.0"
is-path-inside@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
dependencies:
path-is-inside "^1.0.1"
is-posix-bracket@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
@@ -1832,11 +1778,6 @@ os-tmpdir@~1.0.2:
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
p-map@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==
parse-glob@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
@@ -1857,11 +1798,6 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-is-inside@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
pause-stream@0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
@@ -1884,7 +1820,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pify@^2.0.0, pify@^2.3.0:
pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
@@ -2182,7 +2118,7 @@ requires-port@~1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
rimraf@2, rimraf@^2.2.8:
rimraf@2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==

View File

@@ -2,7 +2,7 @@
"name": "agent",
"displayName": "SQL Server Agent",
"description": "Manage and troubleshoot SQL Server Agent jobs",
"version": "0.35.0",
"version": "0.35.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",

View File

@@ -69,8 +69,8 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
return this._tokenCache.clear();
}
public getSecurityToken(account: AzureAccount): Thenable<AzureAccountSecurityTokenCollection> {
return this.doIfInitialized(() => this.getAccessTokens(account));
public getSecurityToken(account: AzureAccount, resource: sqlops.AzureResource): Thenable<AzureAccountSecurityTokenCollection> {
return this.doIfInitialized(() => this.getAccessTokens(account, resource));
}
public initialize(restoredAccounts: sqlops.Account[]): Thenable<sqlops.Account[]> {
@@ -90,7 +90,7 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
// Attempt to get fresh tokens. If this fails then the account is stale.
// NOTE: Based on ADAL implementation, getting tokens should use the refresh token if necessary
let task = this.getAccessTokens(account)
let task = this.getAccessTokens(account, sqlops.AzureResource.ResourceManagement)
.then(
() => {
return account;
@@ -161,9 +161,14 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
: Promise.reject(localize('accountProviderNotInitialized', 'Account provider not initialized, cannot perform action'));
}
private getAccessTokens(account: AzureAccount): Thenable<AzureAccountSecurityTokenCollection> {
private getAccessTokens(account: AzureAccount, resource: sqlops.AzureResource): Thenable<AzureAccountSecurityTokenCollection> {
let self = this;
const resourceIdMap = new Map<sqlops.AzureResource, string>([
[sqlops.AzureResource.ResourceManagement, self._metadata.settings.armResource.id],
[sqlops.AzureResource.Sql, self._metadata.settings.sqlResource.id]
]);
let accessTokenPromises: Thenable<void>[] = [];
let tokenCollection: AzureAccountSecurityTokenCollection = {};
for (let tenant of account.properties.tenants) {
@@ -172,7 +177,7 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
let context = new adal.AuthenticationContext(authorityUrl, null, self._tokenCache);
context.acquireToken(
self._metadata.settings.armResource.id,
resourceIdMap.get(resource),
tenant.userId,
self._metadata.settings.clientId,
(error: Error, response: adal.TokenResponse | adal.ErrorResponse) => {

View File

@@ -81,6 +81,11 @@ export interface Settings {
*/
armResource?: Resource;
/**
* Information that describes the SQL Azure resource
*/
sqlResource?: Resource;
/**
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
* instead of querying the tenants endpoint of the armResource

View File

@@ -27,6 +27,10 @@ const publicAzureSettings: ProviderSettings = {
id: 'https://management.core.windows.net/',
endpoint: 'https://management.azure.com'
},
sqlResource: {
id: 'https://database.windows.net/',
endpoint: 'https://database.windows.net'
},
redirectUri: 'http://localhost/redirect'
}
}

View File

@@ -212,8 +212,8 @@ export class ApiWrapper {
return sqlops.accounts.getAllAccounts();
}
public getSecurityToken(account: sqlops.Account): Thenable<{}> {
return sqlops.accounts.getSecurityToken(account);
public getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}> {
return sqlops.accounts.getSecurityToken(account, resource);
}
public readonly onDidChangeAccounts = sqlops.accounts.onDidChangeAccounts;

View File

@@ -6,7 +6,7 @@
'use strict';
import { window, QuickPickItem } from 'vscode';
import { IConnectionProfile } from 'sqlops';
import * as sqlops from 'sqlops';
import { generateGuid } from './utils';
import { ApiWrapper } from '../apiWrapper';
import { TreeNode } from '../treeNodes';
@@ -30,7 +30,7 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur
let subscriptions = await accountNode.getCachedSubscriptions();
if (!subscriptions || subscriptions.length === 0) {
const credentials = await servicePool.credentialService.getCredentials(accountNode.account);
const credentials = await servicePool.credentialService.getCredentials(accountNode.account, sqlops.AzureResource.ResourceManagement);
subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials);
}
@@ -71,7 +71,7 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur
});
apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => {
let connectionProfile: IConnectionProfile = {
let connectionProfile: sqlops.IConnectionProfile = {
id: generateGuid(),
connectionName: undefined,
serverName: undefined,

View File

@@ -6,29 +6,29 @@
'use strict';
import { ServiceClientCredentials } from 'ms-rest';
import { Account, DidChangeAccountsParams } from 'sqlops';
import * as sqlops from 'sqlops';
import { Event } from 'vscode';
import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models';
export interface IAzureResourceAccountService {
getAccounts(): Promise<Account[]>;
getAccounts(): Promise<sqlops.Account[]>;
readonly onDidChangeAccounts: Event<DidChangeAccountsParams>;
readonly onDidChangeAccounts: Event<sqlops.DidChangeAccountsParams>;
}
export interface IAzureResourceCredentialService {
getCredentials(account: Account): Promise<ServiceClientCredentials[]>;
getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise<ServiceClientCredentials[]>;
}
export interface IAzureResourceSubscriptionService {
getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise<AzureResourceSubscription[]>;
getSubscriptions(account: sqlops.Account, credentials: ServiceClientCredentials[]): Promise<AzureResourceSubscription[]>;
}
export interface IAzureResourceSubscriptionFilterService {
getSelectedSubscriptions(account: Account): Promise<AzureResourceSubscription[]>;
getSelectedSubscriptions(account: sqlops.Account): Promise<AzureResourceSubscription[]>;
saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise<void>;
saveSelectedSubscriptions(account: sqlops.Account, selectedSubscriptions: AzureResourceSubscription[]): Promise<void>;
}
export interface IAzureResourceDatabaseServerService {

View File

@@ -5,7 +5,7 @@
'use strict';
import { Account } from 'sqlops';
import * as sqlops from 'sqlops';
import { TokenCredentials, ServiceClientCredentials } from 'ms-rest';
import { ApiWrapper } from '../../apiWrapper';
import * as nls from 'vscode-nls';
@@ -21,10 +21,10 @@ export class AzureResourceCredentialService implements IAzureResourceCredentialS
this._apiWrapper = apiWrapper;
}
public async getCredentials(account: Account): Promise<ServiceClientCredentials[]> {
public async getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise<ServiceClientCredentials[]> {
try {
let credentials: TokenCredentials[] = [];
let tokens = await this._apiWrapper.getSecurityToken(account);
let tokens = await this._apiWrapper.getSecurityToken(account, resource);
for (let tenant of account.properties.tenants) {
let token = tokens[tenant.id].token;

View File

@@ -5,7 +5,7 @@
'use strict';
import { Account } from 'sqlops';
import * as sqlops from 'sqlops';
import { ServiceClientCredentials } from 'ms-rest';
import { TreeNode } from '../../treeNodes';
@@ -28,7 +28,7 @@ export abstract class AzureResourceTreeNodeBase extends TreeNode {
export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase {
public constructor(
public readonly account: Account,
public readonly account: sqlops.Account,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
@@ -45,7 +45,7 @@ export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTr
protected async getCredentials(): Promise<ServiceClientCredentials[]> {
try {
return await this.servicePool.credentialService.getCredentials(this.account);
return await this.servicePool.credentialService.getCredentials(this.account, sqlops.AzureResource.ResourceManagement);
} catch (error) {
if (error instanceof AzureResourceCredentialError) {
this.servicePool.contextService.showErrorMessage(error.message);

View File

@@ -87,7 +87,7 @@ describe('AzureResourceAccountTreeNode.info', function(): void {
mockServicePool.subscriptionService = mockSubscriptionService.object;
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions);
});
@@ -164,7 +164,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
mockServicePool.subscriptionService = mockSubscriptionService.object;
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions);
});
@@ -177,7 +177,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
const children = await accountTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
@@ -213,7 +213,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
await accountTreeNode.getChildren();
const children = await accountTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1));
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1));
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.exactly(1));
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
@@ -267,7 +267,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
const children = await accountTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());

View File

@@ -118,7 +118,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
mockServicePool.credentialService = mockCredentialService.object;
mockServicePool.databaseService = mockDatabaseService.object;
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseContainerCache);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseContainerCache.databases[mockSubscription.id] = mockDatabases);
});
@@ -130,7 +130,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
const children = await databaseContainerTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
@@ -160,7 +160,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
await databaseContainerTreeNode.getChildren();
const children = await databaseContainerTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1));
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1));
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1));
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
@@ -193,7 +193,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseContainerTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());

View File

@@ -118,7 +118,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
mockServicePool.credentialService = mockCredentialService.object;
mockServicePool.databaseServerService = mockDatabaseServerService.object;
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseServerContainerCache);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseServerContainerCache.databaseServers[mockSubscription.id] = mockDatabaseServers);
});
@@ -130,7 +130,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
const children = await databaseServerContainerTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
@@ -160,7 +160,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
await databaseServerContainerTreeNode.getChildren();
const children = await databaseServerContainerTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1));
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1));
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1));
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
@@ -193,7 +193,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseServerContainerTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());

View File

@@ -2,7 +2,7 @@
"name": "import",
"displayName": "SQL Server Import",
"description": "SQL Server Import for Azure Data Studio supports importing CSV or JSON files into SQL Server.",
"version": "0.4.0",
"version": "0.4.2",
"publisher": "Microsoft",
"preview": true,
"engines": {
@@ -33,6 +33,15 @@
"light": "./images/light_icon.svg",
"dark": "./images/dark_icon.svg"
}
},
{
"command": "dacFx.start",
"title": "Data-tier Application Wizard",
"category": "Data-tier Application",
"icon": {
"light": "./images/light_icon.svg",
"dark": "./images/dark_icon.svg"
}
}
],
"keybindings": [
@@ -48,12 +57,17 @@
"command": "flatFileImport.start",
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database",
"group": "import"
},
{
"command": "dacFx.start",
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database",
"group": "export"
}
]
}
},
"dependencies": {
"dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.10",
"dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.7",
"opener": "^1.4.3",
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
"vscode-extension-telemetry": "0.0.18",

View File

@@ -13,6 +13,7 @@ import { FlatFileWizard } from '../wizard/flatFileWizard';
import { ServiceClient } from '../services/serviceClient';
import { ApiType, managerInstance } from '../services/serviceApiManager';
import { FlatFileProvider } from '../services/contracts';
import { DataTierApplicationWizard } from '../wizard/dataTierApplicationWizard';
/**
* The main controller class that initializes the extension
@@ -35,10 +36,15 @@ export default class MainController extends ControllerBase {
this.initializeFlatFileProvider(provider);
});
this.initializeDacFxWizard();
return Promise.resolve(true);
}
private initializeFlatFileProvider(provider: FlatFileProvider) {
sqlops.tasks.registerTask('flatFileImport.start', (profile: sqlops.IConnectionProfile, ...args: any[]) => new FlatFileWizard(provider).start(profile, args));
}
private initializeDacFxWizard() {
sqlops.tasks.registerTask('dacFx.start', (profile: sqlops.IConnectionProfile, ...args: any[]) => new DataTierApplicationWizard().start(profile, args));
}
}

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 * as sqlops from 'sqlops';
import { BaseDataModel } from './models';
export abstract class BasePage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly model: BaseDataModel;
protected readonly view: sqlops.ModelView;
/**
* This method constructs all the elements of the page.
* @returns {Promise<boolean>}
*/
public async abstract start(): Promise<boolean>;
/**
* This method is called when the user is entering the page.
* @returns {Promise<boolean>}
*/
public async abstract onPageEnter(): Promise<boolean>;
/**
* This method is called when the user is leaving the page.
* @returns {Promise<boolean>}
*/
async onPageLeave(): Promise<boolean> {
return true;
}
/**
* Override this method to cleanup what you don't need cached in the page.
* @returns {Promise<boolean>}
*/
public async cleanup(): Promise<boolean> {
return true;
}
/**
* Sets up a navigation validator.
* This will be called right before onPageEnter().
*/
public abstract setupNavigationValidator();
protected async getServerValues(): Promise<{ connection, displayName, name }[]> {
let cons = await sqlops.connection.getActiveConnections();
// This user has no active connections ABORT MISSION
if (!cons || cons.length === 0) {
return undefined;
}
let count = -1;
let idx = -1;
let values = cons.map(c => {
// Handle the code to remember what the user's choice was from before
count++;
if (idx === -1) {
if (this.model.server && c.connectionId === this.model.server.connectionId) {
idx = count;
} else if (this.model.serverId && c.connectionId === this.model.serverId) {
idx = count;
}
}
let db = c.options.databaseDisplayName;
let usr = c.options.user;
let srv = c.options.server;
if (!db) {
db = '<default>';
}
if (!usr) {
usr = 'default';
}
let finalName = `${srv}, ${db} (${usr})`;
return {
connection: c,
displayName: finalName,
name: c.connectionId
};
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
this.deleteServerValues();
}
return values;
}
protected async getDatabaseValues(): Promise<{ displayName, name }[]> {
let idx = -1;
let count = -1;
let values = (await sqlops.connection.listDatabases(this.model.server.connectionId)).map(db => {
count++;
if (this.model.database && db === this.model.database) {
idx = count;
}
return {
displayName: db,
name: db
};
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
this.deleteDatabaseValues();
}
return values;
}
protected deleteServerValues() {
delete this.model.server;
delete this.model.serverId;
delete this.model.database;
}
protected deleteDatabaseValues() {
return;
}
}

View File

@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
import * as os from 'os';
import * as path from 'path';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxDataModel } from './models';
import { BasePage } from './basePage';
const localize = nls.loadMessageBundle();
export abstract class DacFxConfigPage extends BasePage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
protected serverDropdown: sqlops.DropDownComponent;
protected databaseTextBox: sqlops.InputBoxComponent;
protected databaseDropdown: sqlops.DropDownComponent;
protected databaseLoader: sqlops.LoadingComponent;
protected fileTextBox: sqlops.InputBoxComponent;
protected fileButton: sqlops.ButtonComponent;
protected fileExtension: string;
protected constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super();
this.instance = instance;
this.wizardPage = wizardPage;
this.model = model;
this.view = view;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
return true;
});
}
protected async createServerDropdown(isTargetServer: boolean): Promise<sqlops.FormComponent> {
this.serverDropdown = this.view.modelBuilder.dropDown().withProperties({
required: true
}).component();
// Handle server changes
this.serverDropdown.onValueChanged(async () => {
this.model.server = (this.serverDropdown.value as ConnectionDropdownValue).connection;
this.model.serverName = (this.serverDropdown.value as ConnectionDropdownValue).displayName;
await this.populateDatabaseDropdown();
});
let targetServerTitle = localize('dacFx.targetServerDropdownTitle', 'Target Server');
let sourceServerTitle = localize('dacFx.sourceServerDropdownTitle', 'Source Server');
return {
component: this.serverDropdown,
title: isTargetServer ? targetServerTitle : sourceServerTitle
};
}
protected async populateServerDropdown(): Promise<boolean> {
let values = await this.getServerValues();
if (values === undefined) {
return false;
}
this.model.server = values[0].connection;
this.model.serverName = values[0].displayName;
this.serverDropdown.updateProperties({
values: values
});
return true;
}
protected async createDatabaseTextBox(): Promise<sqlops.FormComponent> {
this.databaseTextBox = this.view.modelBuilder.inputBox().withProperties({
required: true
}).component();
this.databaseTextBox.onTextChanged(async () => {
this.model.database = this.databaseTextBox.value;
});
return {
component: this.databaseTextBox,
title: localize('dacFx.databaseNameTextBox', 'Target Database')
};
}
protected async createDatabaseDropdown(): Promise<sqlops.FormComponent> {
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
required: true
}).component();
// Handle database changes
this.databaseDropdown.onValueChanged(async () => {
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
this.fileTextBox.value = this.generateFilePath();
this.model.filePath = this.fileTextBox.value;
});
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
return {
component: this.databaseLoader,
title: localize('dacFx.sourceDatabaseDropdownTitle', 'Source Database')
};
}
protected async populateDatabaseDropdown(): Promise<boolean> {
this.databaseLoader.loading = true;
this.databaseDropdown.updateProperties({ values: [] });
if (!this.model.server) {
this.databaseLoader.loading = false;
return false;
}
let values = await this.getDatabaseValues();
this.model.database = values[0].name;
this.model.filePath = this.generateFilePath();
this.fileTextBox.value = this.model.filePath;
this.databaseDropdown.updateProperties({
values: values
});
this.databaseLoader.loading = false;
return true;
}
protected async createFileBrowserParts() {
this.fileTextBox = this.view.modelBuilder.inputBox().withProperties({
required: true
}).component();
this.fileButton = this.view.modelBuilder.button().withProperties({
label: '•••',
}).component();
}
protected generateFilePath(): string {
let now = new Date();
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes();
return path.join(os.homedir(), this.model.database + '-' + datetime + this.fileExtension);
}
}
interface ConnectionDropdownValue extends sqlops.CategoryValue {
connection: sqlops.connection.Connection;
}

View File

@@ -8,8 +8,9 @@ import { ImportDataModel } from './models';
import * as sqlops from 'sqlops';
import { FlatFileProvider } from '../../services/contracts';
import { FlatFileWizard } from '../flatFileWizard';
import { BasePage } from './basePage';
export abstract class ImportPage {
export abstract class ImportPage extends BasePage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: FlatFileWizard;
@@ -18,42 +19,11 @@ export abstract class ImportPage {
protected readonly provider: FlatFileProvider;
protected constructor(instance: FlatFileWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) {
super();
this.instance = instance;
this.wizardPage = wizardPage;
this.model = model;
this.view = view;
this.provider = provider;
}
/**
* This method constructs all the elements of the page.
* @returns {Promise<boolean>}
*/
public async abstract start(): Promise<boolean>;
/**
* This method is called when the user is entering the page.
* @returns {Promise<boolean>}
*/
public async abstract onPageEnter(): Promise<boolean>;
/**
* This method is called when the user is leaving the page.
* @returns {Promise<boolean>}
*/
public async abstract onPageLeave(): Promise<boolean>;
/**
* Sets up a navigation validator.
* This will be called right before onPageEnter().
*/
public abstract setupNavigationValidator();
/**
* Override this method to cleanup what you don't need cached in the page.
* @returns {Promise<boolean>}
*/
public async cleanup(): Promise<boolean> {
return true;
}
}

View File

@@ -6,15 +6,19 @@
import * as sqlops from 'sqlops';
export interface BaseDataModel {
server: sqlops.connection.Connection;
serverId: string;
database: string;
}
/**
* The main data model that communicates between the pages.
*/
export interface ImportDataModel {
export interface ImportDataModel extends BaseDataModel {
ownerUri: string;
proseColumns: ColumnMetadata[];
proseDataPreview: string[][];
server: sqlops.connection.Connection;
serverId: string;
database: string;
table: string;
schema: string;
@@ -31,3 +35,14 @@ export interface ColumnMetadata {
primaryKey: boolean;
nullable: boolean;
}
/**
* Data model to communicate between DacFx pages
*/
export interface DacFxDataModel extends BaseDataModel {
serverName: string;
serverId: string;
filePath: string;
version: string;
upgradeExisting: boolean;
}

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 vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as sqlops from 'sqlops';
import { SelectOperationPage } from './pages/selectOperationpage';
import { DeployConfigPage } from './pages/deployConfigPage';
import { DacFxSummaryPage } from './pages/dacFxSummaryPage';
import { ExportConfigPage } from './pages/exportConfigPage';
import { ExtractConfigPage } from './pages/extractConfigPage';
import { ImportConfigPage } from './pages/importConfigPage';
import { DacFxDataModel } from './api/models';
import { BasePage } from './api/basePage';
const localize = nls.loadMessageBundle();
class Page {
wizardPage: sqlops.window.modelviewdialog.WizardPage;
dacFxPage: BasePage;
constructor(wizardPage: sqlops.window.modelviewdialog.WizardPage) {
this.wizardPage = wizardPage;
}
}
export enum Operation {
deploy,
extract,
import,
export
}
export class DataTierApplicationWizard {
public wizard: sqlops.window.modelviewdialog.Wizard;
private connection: sqlops.connection.Connection;
private model: DacFxDataModel;
public pages: Map<string, Page> = new Map<string, Page>();
public selectedOperation: Operation;
constructor() {
}
public async start(p: any, ...args: any[]) {
this.model = <DacFxDataModel>{};
let profile = p ? <sqlops.IConnectionProfile>p.connectionProfile : undefined;
if (profile) {
this.model.serverId = profile.id;
this.model.database = profile.databaseName;
}
this.connection = await sqlops.connection.getCurrentConnection();
if (!this.connection) {
this.connection = await sqlops.connection.openConnectionDialog();
}
this.wizard = sqlops.window.modelviewdialog.createWizard('Data-tier Application Wizard');
let selectOperationWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation'));
let deployConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings'));
let summaryWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.summaryPageName', 'Summary'));
let extractConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings'));
let importConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.importConfigPageName', 'Select Import Bacpac Settings'));
let exportConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.exportConfigPageName', 'Select Export Bacpac Settings'));
this.pages.set('selectOperation', new Page(selectOperationWizardPage));
this.pages.set('deployConfig', new Page(deployConfigWizardPage));
this.pages.set('extractConfig', new Page(extractConfigWizardPage));
this.pages.set('importConfig', new Page(importConfigWizardPage));
this.pages.set('exportConfig', new Page(exportConfigWizardPage));
this.pages.set('summary', new Page(summaryWizardPage));
selectOperationWizardPage.registerContent(async (view) => {
let selectOperationDacFxPage = new SelectOperationPage(this, selectOperationWizardPage, this.model, view);
this.pages.get('selectOperation').dacFxPage = selectOperationDacFxPage;
await selectOperationDacFxPage.start().then(() => {
selectOperationDacFxPage.setupNavigationValidator();
selectOperationDacFxPage.onPageEnter();
});
});
deployConfigWizardPage.registerContent(async (view) => {
let deployConfigDacFxPage = new DeployConfigPage(this, deployConfigWizardPage, this.model, view);
this.pages.get('deployConfig').dacFxPage = deployConfigDacFxPage;
await deployConfigDacFxPage.start();
});
extractConfigWizardPage.registerContent(async (view) => {
let extractConfigDacFxPage = new ExtractConfigPage(this, extractConfigWizardPage, this.model, view);
this.pages.get('extractConfig').dacFxPage = extractConfigDacFxPage;
await extractConfigDacFxPage.start();
});
importConfigWizardPage.registerContent(async (view) => {
let importConfigDacFxPage = new ImportConfigPage(this, importConfigWizardPage, this.model, view);
this.pages.get('importConfig').dacFxPage = importConfigDacFxPage;
await importConfigDacFxPage.start();
});
exportConfigWizardPage.registerContent(async (view) => {
let exportConfigDacFxPage = new ExportConfigPage(this, exportConfigWizardPage, this.model, view);
this.pages.get('exportConfig').dacFxPage = exportConfigDacFxPage;
await exportConfigDacFxPage.start();
});
summaryWizardPage.registerContent(async (view) => {
let summaryDacFxPage = new DacFxSummaryPage(this, summaryWizardPage, this.model, view);
this.pages.get('summary').dacFxPage = summaryDacFxPage;
await summaryDacFxPage.start();
});
this.wizard.onPageChanged(async (event) => {
let idx = event.newPage;
let page: Page;
if (idx === 1) {
switch (this.selectedOperation) {
case Operation.deploy: {
page = this.pages.get('deployConfig');
break;
}
case Operation.extract: {
page = this.pages.get('extractConfig');
break;
}
case Operation.import: {
page = this.pages.get('importConfig');
break;
}
case Operation.export: {
page = this.pages.get('exportConfig');
break;
}
}
} else if (idx === 2) {
page = this.pages.get('summary');
}
if (page !== undefined) {
page.dacFxPage.setupNavigationValidator();
page.dacFxPage.onPageEnter();
}
});
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, summaryWizardPage];
this.wizard.generateScriptButton.hidden = true;
this.wizard.doneButton.onClick(async () => await this.executeOperation());
this.wizard.open();
}
public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean) {
this.wizard.registerNavigationValidator(validator);
}
public setDoneButton(operation: Operation): void {
switch (operation) {
case Operation.deploy: {
this.wizard.doneButton.label = localize('dacFx.deployButton', 'Deploy');
this.selectedOperation = Operation.deploy;
break;
}
case Operation.extract: {
this.wizard.doneButton.label = localize('dacFx.extractButton', 'Extract');
this.selectedOperation = Operation.extract;
break;
}
case Operation.import: {
this.wizard.doneButton.label = localize('dacFx.importButton', 'Import');
this.selectedOperation = Operation.import;
break;
}
case Operation.export: {
this.wizard.doneButton.label = localize('dacFx.exportButton', 'Export');
this.selectedOperation = Operation.export;
break;
}
}
}
private async executeOperation() {
switch (this.selectedOperation) {
case Operation.deploy: {
await this.deploy();
break;
}
case Operation.extract: {
await this.extract();
break;
}
case Operation.import: {
await this.import();
break;
}
case Operation.export: {
await this.export();
break;
}
}
}
private async deploy() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.deployErrorMessage', "Deploy failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async extract() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.extractErrorMessage', "Extract failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async export() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.exportErrorMessage', "Export failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async import() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.importErrorMessage', "Import failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
public static async getService(): Promise<sqlops.DacFxServicesProvider> {
let currentConnection = await sqlops.connection.getCurrentConnection();
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(currentConnection.providerName, sqlops.DataProviderType.DacFxServicesProvider);
return service;
}
}

View File

@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
import { BasePage } from '../api/basePage';
const localize = nls.loadMessageBundle();
export class DacFxSummaryPage extends BasePage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private form: sqlops.FormContainer;
private table: sqlops.TableComponent;
private loader: sqlops.LoadingComponent;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super();
this.instance = instance;
this.wizardPage = wizardPage;
this.model = model;
this.view = view;
}
async start(): Promise<boolean> {
this.table = this.view.modelBuilder.table().component();
this.loader = this.view.modelBuilder.loadingComponent().withItem(this.table).component();
this.form = this.view.modelBuilder.formContainer().withFormItems(
[
{
component: this.table,
title: ''
}
]
).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
this.populateTable();
this.loader.loading = false;
return true;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
if (this.loader.loading) {
return false;
}
return true;
});
}
private populateTable() {
let data = [];
let targetServer = localize('dacfx.targetServerName', 'Target Server');
let targetDatabase = localize('dacfx.targetDatabaseName', 'Target Database');
let sourceServer = localize('dacfx.sourceServerName', 'Source Server');
let sourceDatabase = localize('dacfx.sourceDatabaseName', 'Source Database');
let fileLocation = localize('dacfx.fileLocation', 'File Location');
switch (this.instance.selectedOperation) {
case Operation.deploy: {
data = [
[targetServer, this.model.serverName],
[fileLocation, this.model.filePath],
[targetDatabase, this.model.database]];
break;
}
case Operation.extract: {
data = [
[sourceServer, this.model.serverName],
[sourceDatabase, this.model.database],
[localize('dacfxExtract.version', 'Version'), this.model.version],
[fileLocation, this.model.filePath]];
break;
}
case Operation.import: {
data = [
[targetServer, this.model.serverName],
[fileLocation, this.model.filePath],
[targetDatabase, this.model.database]];
break;
}
case Operation.export: {
data = [
[sourceServer, this.model.serverName],
[sourceDatabase, this.model.database],
[fileLocation, this.model.filePath]];
break;
}
}
this.table.updateProperties({
data: data,
columns: ['Setting', 'Value'],
width: 600,
height: 200
});
}
}

View File

@@ -0,0 +1,190 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class DeployConfigPage extends DacFxConfigPage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private databaseDropdownComponent: sqlops.FormComponent;
private databaseComponent: sqlops.FormComponent;
private formBuilder: sqlops.FormBuilder;
private form: sqlops.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.bacpac';
}
async start(): Promise<boolean> {
let serverComponent = await this.createServerDropdown(true);
let fileBrowserComponent = await this.createFileBrowser();
this.databaseComponent = await this.createDatabaseTextBox();
this.databaseComponent.title = localize('dacFx.databaseNameTextBox', 'Database Name');
this.databaseDropdownComponent = await this.createDeployDatabaseDropdown();
this.databaseDropdownComponent.title = localize('dacFx.databaseNameDropdown', 'Database Name');
let radioButtons = await this.createRadiobuttons();
this.formBuilder = this.view.modelBuilder.formContainer()
.withFormItems(
[
fileBrowserComponent,
serverComponent,
radioButtons,
this.databaseDropdownComponent
], {
horizontal: true,
componentWidth: 400
});
this.form = this.formBuilder.component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
let r2 = await this.populateDeployDatabaseDropdown();
return r1 && r2;
}
private async createFileBrowser(): Promise<sqlops.FormComponent> {
this.createFileBrowserParts();
this.fileButton.onDidClick(async (click) => {
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(os.homedir()),
openLabel: localize('dacFxDeploy.openFile', 'Open'),
filters: {
'dacpac Files': ['dacpac'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
this.databaseTextBox.value = this.generateDatabaseName(this.model.filePath);
if (!this.model.upgradeExisting) {
this.model.database = this.databaseTextBox.value;
}
});
return {
component: this.fileTextBox,
title: localize('dacFxDeploy.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
private async createRadiobuttons(): Promise<sqlops.FormComponent> {
let upgradeRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'updateExisting',
label: localize('dacFx.upgradeRadioButtonLabel', 'Upgrade Existing Database'),
}).component();
let newRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'updateExisting',
label: localize('dacFx.newRadioButtonLabel', 'New Database'),
}).component();
upgradeRadioButton.onDidClick(() => {
this.model.upgradeExisting = true;
this.formBuilder.removeFormItem(this.databaseComponent);
this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 });
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
});
newRadioButton.onDidClick(() => {
this.model.upgradeExisting = false;
this.formBuilder.removeFormItem(this.databaseDropdownComponent);
this.formBuilder.addFormItem(this.databaseComponent, { horizontal: true, componentWidth: 400 });
this.model.database = this.databaseTextBox.value;
});
// Initialize with upgrade existing true
upgradeRadioButton.checked = true;
this.model.upgradeExisting = true;
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
}).withItems([
upgradeRadioButton, newRadioButton]
).component();
return {
component: flexRadioButtonsModel,
title: localize('dacFx.targetDatabaseRadioButtonsTitle', 'Target Database')
};
}
protected async createDeployDatabaseDropdown(): Promise<sqlops.FormComponent> {
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
required: true
}).component();
// Handle database changes
this.databaseDropdown.onValueChanged(async () => {
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
});
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
return {
component: this.databaseLoader,
title: localize('dacFx.targetDatabaseDropdownTitle', 'Database Name')
};
}
protected async populateDeployDatabaseDropdown(): Promise<boolean> {
this.databaseLoader.loading = true;
this.databaseDropdown.updateProperties({ values: [] });
if (!this.model.server) {
this.databaseLoader.loading = false;
return false;
}
let values = await this.getDatabaseValues();
if (this.model.database === undefined) {
this.model.database = values[0].name;
}
this.databaseDropdown.updateProperties({
values: values
});
this.databaseLoader.loading = false;
return true;
}
private generateDatabaseName(filePath: string): string {
let result = path.parse(filePath);
return result.name;
}
}

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as vscode from 'vscode';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class ExportConfigPage extends DacFxConfigPage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private form: sqlops.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.bacpac';
}
async start(): Promise<boolean> {
let databaseComponent = await this.createDatabaseDropdown();
let serverComponent = await this.createServerDropdown(false);
let fileBrowserComponent = await this.createFileBrowser();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
serverComponent,
databaseComponent,
fileBrowserComponent,
], {
horizontal: true,
componentWidth: 400
}).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
let r2 = await this.populateDatabaseDropdown();
return r1 && r2;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
if (this.databaseLoader.loading) {
return false;
}
return true;
});
}
private async createFileBrowser(): Promise<sqlops.FormComponent> {
this.createFileBrowserParts();
// default filepath
this.fileTextBox.value = this.generateFilePath();
this.model.filePath = this.fileTextBox.value;
this.fileButton.onDidClick(async (click) => {
let fileUri = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(this.fileTextBox.value),
saveLabel: localize('dacfxExport.saveFile', 'Save'),
filters: {
'bacpac Files': ['bacpac'],
}
}
);
if (!fileUri) {
return;
}
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
});
return {
component: this.fileTextBox,
title: localize('dacFxExport.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class ExtractConfigPage extends DacFxConfigPage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private form: sqlops.FormContainer;
private versionTextBox: sqlops.InputBoxComponent;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.dacpac';
}
async start(): Promise<boolean> {
let databaseComponent = await this.createDatabaseDropdown();
let serverComponent = await this.createServerDropdown(false);
let fileBrowserComponent = await this.createFileBrowser();
let versionComponent = await this.createVersionTextBox();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
serverComponent,
databaseComponent,
versionComponent,
fileBrowserComponent,
], {
horizontal: true,
componentWidth: 400
}).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
let r2 = await this.populateDatabaseDropdown();
return r1 && r2;
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
if (this.databaseLoader.loading) {
return false;
}
return true;
});
}
private async createFileBrowser(): Promise<sqlops.FormComponent> {
this.createFileBrowserParts();
// default filepath
this.fileTextBox.value = this.generateFilePath();
this.model.filePath = this.fileTextBox.value;
this.fileButton.onDidClick(async (click) => {
let fileUri = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(this.fileTextBox.value),
saveLabel: localize('dacfxExtract.saveFile', 'Save'),
filters: {
'dacpac Files': ['dacpac'],
}
}
);
if (!fileUri) {
return;
}
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
});
return {
component: this.fileTextBox,
title: localize('dacFxExtract.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
private async createVersionTextBox(): Promise<sqlops.FormComponent> {
this.versionTextBox = this.view.modelBuilder.inputBox().withProperties({
required: true
}).component();
// default filepath
this.versionTextBox.value = '1.0.0.0';
this.model.version = this.versionTextBox.value;
this.versionTextBox.onTextChanged(async () => {
this.model.version = this.versionTextBox.value;
});
return {
component: this.versionTextBox,
title: localize('dacFxExtract.versionTextboxTitle', 'Version (use x.x.x.x where x is a number)'),
};
}
}

View File

@@ -102,57 +102,9 @@ export class FileConfigPage extends ImportPage {
}
private async populateServerDropdown(): Promise<boolean> {
let cons = await sqlops.connection.getActiveConnections();
// This user has no active connections ABORT MISSION
if (!cons || cons.length === 0) {
return true;
}
let count = -1;
let idx = -1;
let values = cons.map(c => {
// Handle the code to remember what the user's choice was from before
count++;
if (idx === -1) {
if (this.model.server && c.connectionId === this.model.server.connectionId) {
idx = count;
} else if (this.model.serverId && c.connectionId === this.model.serverId) {
idx = count;
}
}
let db = c.options.databaseDisplayName;
let usr = c.options.user;
let srv = c.options.server;
if (!db) {
db = '<default>';
}
if (!usr) {
usr = 'default';
}
let finalName = `${srv}, ${db} (${usr})`;
return {
connection: c,
displayName: finalName,
name: c.connectionId
};
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
delete this.model.server;
delete this.model.serverId;
delete this.model.database;
delete this.model.schema;
let values = await this.getServerValues();
if (values === undefined) {
return false;
}
this.model.server = values[0].connection;
@@ -195,29 +147,7 @@ export class FileConfigPage extends ImportPage {
return false;
}
let idx = -1;
let count = -1;
let values = (await sqlops.connection.listDatabases(this.model.server.connectionId)).map(db => {
count++;
if (this.model.database && db === this.model.database) {
idx = count;
}
return {
displayName: db,
name: db
};
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
delete this.model.database;
delete this.model.schema;
}
let values = await this.getDatabaseValues();
this.model.database = values[0].name;
@@ -377,6 +307,18 @@ export class FileConfigPage extends ImportPage {
return true;
}
protected deleteServerValues() {
delete this.model.server;
delete this.model.serverId;
delete this.model.database;
delete this.model.schema;
}
protected deleteDatabaseValues() {
delete this.model.database;
delete this.model.schema;
}
// private async populateTableNames(): Promise<boolean> {
// this.tableNames = [];
// let databaseName = (<sqlops.CategoryValue>this.databaseDropdown.value).name;

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class ImportConfigPage extends DacFxConfigPage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private form: sqlops.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.bacpac';
}
async start(): Promise<boolean> {
let databaseComponent = await this.createDatabaseTextBox();
let serverComponent = await this.createServerDropdown(true);
let fileBrowserComponent = await this.createFileBrowser();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
fileBrowserComponent,
serverComponent,
databaseComponent,
], {
horizontal: true,
componentWidth: 400
}).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
return r1;
}
private async createFileBrowser(): Promise<sqlops.FormComponent> {
this.createFileBrowserParts();
this.fileButton.onDidClick(async (click) => {
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(os.homedir()),
openLabel: localize('dacFxImport.openFile', 'Open'),
filters: {
'bacpac Files': ['bacpac'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
this.model.database = this.generateDatabaseName(this.model.filePath);
this.databaseTextBox.value = this.model.database;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
this.model.database = this.generateDatabaseName(this.model.filePath);
this.databaseTextBox.value = this.model.database;
});
return {
component: this.fileTextBox,
title: localize('dacFxImport.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
private generateDatabaseName(filePath: string): string {
let result = path.parse(filePath);
return result.name;
}
}

View File

@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard, Operation } from '../DataTierApplicationWizard';
import { BasePage } from '../api/basePage';
const localize = nls.loadMessageBundle();
export class SelectOperationPage extends BasePage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private deployRadioButton: sqlops.RadioButtonComponent;
private extractRadioButton: sqlops.RadioButtonComponent;
private importRadioButton: sqlops.RadioButtonComponent;
private exportRadioButton: sqlops.RadioButtonComponent;
private form: sqlops.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super();
this.instance = instance;
this.wizardPage = wizardPage;
this.model = model;
this.view = view;
}
async start(): Promise<boolean> {
let deployComponent = await this.createDeployRadioButton();
let extractComponent = await this.createExtractRadioButton();
let importComponent = await this.createImportRadioButton();
let exportComponent = await this.createExportRadioButton();
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
deployComponent,
extractComponent,
importComponent,
exportComponent
], {
horizontal: true
}).component();
await this.view.initializeModel(this.form);
// default have the first radio button checked
this.deployRadioButton.checked = true;
this.instance.setDoneButton(Operation.deploy);
return true;
}
async onPageEnter(): Promise<boolean> {
let numPages = this.instance.wizard.pages.length;
for (let i = numPages - 1; i > 2; --i) {
await this.instance.wizard.removePage(i);
}
return true;
}
private async createDeployRadioButton(): Promise<sqlops.FormComponent> {
this.deployRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.deployRadioButtonLabel', 'Deploy a data-tier application .dacpac file to an instance of SQL Server [Deploy Dacpac]'),
}).component();
this.deployRadioButton.onDidClick(() => {
// remove the previous page
this.instance.wizard.removePage(1);
// add deploy page
let page = this.instance.pages.get('deployConfig');
this.instance.wizard.addPage(page.wizardPage, 1);
// change button text and operation
this.instance.setDoneButton(Operation.deploy);
});
return {
component: this.deployRadioButton,
title: ''
};
}
private async createExtractRadioButton(): Promise<sqlops.FormComponent> {
this.extractRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.extractRadioButtonLabel', 'Extract a data-tier application from an instance of SQL Server to a .dacpac file [Extract Dacpac]'),
}).component();
this.extractRadioButton.onDidClick(() => {
// remove the previous pages
this.instance.wizard.removePage(1);
// add the extract page
let page = this.instance.pages.get('extractConfig');
this.instance.wizard.addPage(page.wizardPage, 1);
// change button text and operation
this.instance.setDoneButton(Operation.extract);
});
return {
component: this.extractRadioButton,
title: ''
};
}
private async createImportRadioButton(): Promise<sqlops.FormComponent> {
this.importRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.importRadioButtonLabel', 'Create a database from a .bacpac file [Import Bacpac]'),
}).component();
this.importRadioButton.onDidClick(() => {
// remove the previous page
this.instance.wizard.removePage(1);
// add the import page
let page = this.instance.pages.get('importConfig');
this.instance.wizard.addPage(page.wizardPage, 1);
// change button text and operation
this.instance.setDoneButton(Operation.import);
});
return {
component: this.importRadioButton,
title: ''
};
}
private async createExportRadioButton(): Promise<sqlops.FormComponent> {
this.exportRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'selectedOperation',
label: localize('dacFx.exportRadioButtonLabel', 'Export the schema and data from a database to the logical .bacpac file format [Export Bacpac]'),
}).component();
this.exportRadioButton.onDidClick(() => {
// remove the 2 previous pages
this.instance.wizard.removePage(1);
// add the export pages
let page = this.instance.pages.get('exportConfig');
this.instance.wizard.addPage(page.wizardPage, 1);
// change button text and operation
this.instance.setDoneButton(Operation.export);
});
return {
component: this.exportRadioButton,
title: ''
};
}
public setupNavigationValidator() {
this.instance.registerNavigationValidator(() => {
return true;
});
}
}

View File

@@ -11,7 +11,7 @@ agent-base@4, agent-base@^4.1.0:
applicationinsights@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927"
resolved "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927"
integrity sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc=
dependencies:
diagnostic-channel "0.2.0"
@@ -36,7 +36,7 @@ buffer-alloc-unsafe@^1.1.0:
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
buffer-alloc@^1.2.0:
buffer-alloc@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
@@ -75,26 +75,19 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.10":
version "0.2.10"
resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/4de3f7caf0eba54159911b977ddb4f5d7c0a9ca8"
"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.7":
version "0.2.6"
resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/85653d8b305af8aef334728d71f07bdc240dfcb7"
dependencies:
vscode-languageclient "3.5.1"
debug@3.1.0:
debug@3.1.0, debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.1.0:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
dependencies:
ms "^2.1.1"
decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1"
@@ -150,12 +143,12 @@ decompress@^4.2.0:
diagnostic-channel-publishers@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
resolved "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM=
diagnostic-channel@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
resolved "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
dependencies:
semver "^5.3.0"
@@ -168,9 +161,9 @@ end-of-stream@^1.0.0:
once "^1.4.0"
es6-promise@^4.0.3:
version "4.2.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054"
integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==
version "4.2.4"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
integrity sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==
es6-promisify@^5.0.0:
version "5.0.0"
@@ -220,9 +213,9 @@ get-stream@^2.2.0:
pinkie-promise "^2.0.0"
graceful-fs@^4.1.10:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=
"graceful-readlink@>= 1.0.0":
version "1.0.1"
@@ -294,11 +287,6 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -312,9 +300,9 @@ once@^1.4.0:
wrappy "1"
opener@^1.4.3:
version "1.5.1"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==
version "1.4.3"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
integrity sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=
os-tmpdir@~1.0.2:
version "1.0.2"
@@ -380,7 +368,7 @@ seek-bzip@^1.0.5:
semver@^5.3.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
resolved "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
"service-downloader@github:anthonydresser/service-downloader#0.1.5":
@@ -409,16 +397,16 @@ strip-dirs@^2.0.0:
is-natural-number "^4.0.1"
tar-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
version "1.6.1"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.1.tgz#f84ef1696269d6223ca48f6e1eeede3f7e81f395"
integrity sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==
dependencies:
bl "^1.0.0"
buffer-alloc "^1.2.0"
buffer-alloc "^1.1.0"
end-of-stream "^1.0.0"
fs-constants "^1.0.0"
readable-stream "^2.3.0"
to-buffer "^1.1.1"
to-buffer "^1.1.0"
xtend "^4.0.0"
through@^2.3.6:
@@ -433,15 +421,15 @@ tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
to-buffer@^1.1.1:
to-buffer@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
unbzip2-stream@^1.0.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.1.tgz#7854da51622a7e63624221196357803b552966a1"
integrity sha512-fIZnvdjblYs7Cru/xC6tCPVhz7JkYcVQQkePwMLyQELzYTds2Xn8QefPVnvdVhhZqubxNA1cASXEH5wcK0Bucw==
version "1.2.5"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz#73a033a567bbbde59654b193c44d48a7e4f43c47"
integrity sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==
dependencies:
buffer "^3.0.1"
through "^2.3.6"
@@ -453,7 +441,7 @@ util-deprecate@~1.0.1:
vscode-extension-telemetry@0.0.18:
version "0.0.18"
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.18.tgz#602ba20d8c71453aa34533a291e7638f6e5c0327"
resolved "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.18.tgz#602ba20d8c71453aa34533a291e7638f6e5c0327"
integrity sha512-Vw3Sr+dZwl+c6PlsUwrTtCOJkgrmvS3OUVDQGcmpXWAgq9xGq6as0K4pUx+aGqTjzLAESmWSrs6HlJm6J6Khcg==
dependencies:
applicationinsights "1.0.1"
@@ -484,9 +472,9 @@ vscode-languageserver-types@3.5.0:
integrity sha1-5I15li8LjgLelV4/UkkI4rGcA3Q=
vscode-nls@^3.2.1:
version "3.2.5"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
version "3.2.4"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.4.tgz#2166b4183c8aea884d20727f5449e62be69fd398"
integrity sha512-FTjdqa4jDDoBjJqr36O8lmmZf/55kQ2w4ZY/+GL6K92fq765BqO3aYw21atnXUno/P04V5DWagNl4ybDIndJsw==
wrappy@1:
version "1.0.2"
@@ -508,5 +496,5 @@ yauzl@^2.4.2:
zone.js@0.7.6:
version "0.7.6"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"
resolved "https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"
integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=

View File

@@ -18,7 +18,7 @@
"update-grammar": "node ../../build/npm/update-grammar.js Microsoft/vscode-mssql syntaxes/SQL.plist ./syntaxes/sql.tmLanguage.json"
},
"dependencies": {
"dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.10",
"dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.9",
"opener": "^1.4.3",
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
"vscode-extension-telemetry": "^0.0.15"
@@ -285,6 +285,10 @@
{
"displayName": "Windows Authentication",
"name": "Integrated"
},
{
"displayName": "Azure Active Directory - Universal with MFA support",
"name": "AzureMFA"
}
],
"isRequired": true,

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "1.5.0-alpha.58",
"version": "1.5.0-alpha.59",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp2.2.zip",
"Windows_64": "win-x64-netcoreapp2.2.zip",

View File

@@ -291,3 +291,60 @@ export namespace DeleteAgentJobScheduleRequest {
}
// ------------------------------- < Agent Management > ------------------------------------
// ------------------------------- < DacFx > ------------------------------------
export enum TaskExecutionMode {
execute = 0,
script = 1,
executeAndScript = 2,
}
export interface ExportParams {
databaseName: string;
packageFilePath: string;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export interface ImportParams {
packageFilePath: string;
databaseName: string;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export interface ExtractParams {
databaseName: string;
packageFilePath: string;
applicationName: string;
applicationVersion: string;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export interface DeployParams {
packageFilePath: string;
databaseName: string;
upgradeExisting: boolean;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export namespace ExportRequest {
export const type = new RequestType<ExportParams, sqlops.DacFxResult, void, void>('dacfx/export');
}
export namespace ImportRequest {
export const type = new RequestType<ImportParams, sqlops.DacFxResult, void, void>('dacfx/import');
}
export namespace ExtractRequest {
export const type = new RequestType<ExtractParams, sqlops.DacFxResult, void, void>('dacfx/extract');
}
export namespace DeployRequest {
export const type = new RequestType<DeployParams, sqlops.DacFxResult, void, void>('dacfx/deploy');
}
// ------------------------------- < DacFx > ------------------------------------

View File

@@ -8,7 +8,7 @@ import { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client';
import { ClientCapabilities, StaticFeature, RPCMessageType, ServerCapabilities } from 'vscode-languageclient';
import { Disposable } from 'vscode';
import { Telemetry } from './telemetry';
import * as contracts from './contracts';
import * as contracts from './contracts';
import * as sqlops from 'sqlops';
import * as Utils from './utils';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
@@ -28,6 +28,94 @@ export class TelemetryFeature implements StaticFeature {
}
}
export class DacFxServicesFeature extends SqlOpsFeature<undefined> {
private static readonly messageTypes: RPCMessageType[] = [
contracts.ExportRequest.type,
contracts.ImportRequest.type,
contracts.ExtractRequest.type,
contracts.DeployRequest.type
];
constructor(client: SqlOpsDataClient) {
super(client, DacFxServicesFeature.messageTypes);
}
public fillClientCapabilities(capabilities: ClientCapabilities): void {
}
public initialize(capabilities: ServerCapabilities): void {
this.register(this.messages, {
id: UUID.generateUuid(),
registerOptions: undefined
});
}
protected registerProvider(options: undefined): Disposable {
const client = this._client;
let self = this;
let exportBacpac = (databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
let params: contracts.ExportParams = { databaseName: databaseName, packageFilePath: packageFilePath, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
return client.sendRequest(contracts.ExportRequest.type, params).then(
r => {
return r;
},
e => {
client.logFailedRequest(contracts.ExportRequest.type, e);
return Promise.resolve(undefined);
}
);
};
let importBacpac = (packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
let params: contracts.ImportParams = { packageFilePath: packageFilePath, databaseName: databaseName, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
return client.sendRequest(contracts.ImportRequest.type, params).then(
r => {
return r;
},
e => {
client.logFailedRequest(contracts.ImportRequest.type, e);
return Promise.resolve(undefined);
}
);
};
let extractDacpac = (databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
let params: contracts.ExtractParams = { databaseName: databaseName, packageFilePath: packageFilePath, applicationName: applicationName, applicationVersion: applicationVersion, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
return client.sendRequest(contracts.ExtractRequest.type, params).then(
r => {
return r;
},
e => {
client.logFailedRequest(contracts.ExtractRequest.type, e);
return Promise.resolve(undefined);
}
);
};
let deployDacpac = (packageFilePath: string, targetDatabaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
let params: contracts.DeployParams = { packageFilePath: packageFilePath, databaseName: targetDatabaseName, upgradeExisting: upgradeExisting, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
return client.sendRequest(contracts.DeployRequest.type, params).then(
r => {
return r;
},
e => {
client.logFailedRequest(contracts.DeployRequest.type, e);
return Promise.resolve(undefined);
}
);
};
return sqlops.dataprotocol.registerDacFxServicesProvider({
providerId: client.providerId,
exportBacpac,
importBacpac,
extractDacpac,
deployDacpac
});
}
}
export class AgentServicesFeature extends SqlOpsFeature<undefined> {
private static readonly messagesTypes: RPCMessageType[] = [
contracts.AgentJobsRequest.type,
@@ -229,7 +317,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
};
// Alert management methods
let getAlerts = (ownerUri: string): Thenable<sqlops.AgentAlertsResult> => {
let getAlerts = (ownerUri: string): Thenable<sqlops.AgentAlertsResult> => {
let params: contracts.AgentAlertsParams = {
ownerUri: ownerUri
};
@@ -299,7 +387,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
};
// Operator management methods
let getOperators = (ownerUri: string): Thenable<sqlops.AgentOperatorsResult> => {
let getOperators = (ownerUri: string): Thenable<sqlops.AgentOperatorsResult> => {
let params: contracts.AgentOperatorsParams = {
ownerUri: ownerUri
};
@@ -369,7 +457,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
};
// Proxy management methods
let getProxies = (ownerUri: string): Thenable<sqlops.AgentProxiesResult> => {
let getProxies = (ownerUri: string): Thenable<sqlops.AgentProxiesResult> => {
let params: contracts.AgentProxiesParams = {
ownerUri: ownerUri
};
@@ -439,7 +527,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
};
// Agent Credential Method
let getCredentials = (ownerUri: string): Thenable<sqlops.GetCredentialsResult> => {
let getCredentials = (ownerUri: string): Thenable<sqlops.GetCredentialsResult> => {
let params: contracts.GetCredentialsParams = {
ownerUri: ownerUri
};
@@ -455,7 +543,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
// Job Schedule management methods
let getJobSchedules = (ownerUri: string): Thenable<sqlops.AgentJobSchedulesResult> => {
let getJobSchedules = (ownerUri: string): Thenable<sqlops.AgentJobSchedulesResult> => {
let params: contracts.AgentJobScheduleParams = {
ownerUri: ownerUri
};

View File

@@ -16,7 +16,7 @@ import { CredentialStore } from './credentialstore/credentialstore';
import { AzureResourceProvider } from './resourceProvider/resourceProvider';
import * as Utils from './utils';
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
import { TelemetryFeature, AgentServicesFeature } from './features';
import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './features';
const baseConfig = require('./config.json');
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
@@ -55,7 +55,8 @@ export async function activate(context: vscode.ExtensionContext) {
// we only want to add new features
...SqlOpsDataClient.defaultFeatures,
TelemetryFeature,
AgentServicesFeature
AgentServicesFeature,
DacFxServicesFeature,
],
outputChannel: new CustomOutputChannel()
};

View File

@@ -36,7 +36,7 @@ buffer-alloc-unsafe@^1.1.0:
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
buffer-alloc@^1.2.0:
buffer-alloc@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
@@ -75,26 +75,19 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.10":
version "0.2.10"
resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/4de3f7caf0eba54159911b977ddb4f5d7c0a9ca8"
"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.9":
version "0.2.9"
resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/0a3c0f22940d1c67bb567171508ccb1169c6313a"
dependencies:
vscode-languageclient "3.5.1"
debug@3.1.0:
debug@3.1.0, debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.1.0:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
dependencies:
ms "^2.1.1"
decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1"
@@ -168,9 +161,9 @@ end-of-stream@^1.0.0:
once "^1.4.0"
es6-promise@^4.0.3:
version "4.2.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054"
integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==
version "4.2.4"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
integrity sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==
es6-promisify@^5.0.0:
version "5.0.0"
@@ -220,9 +213,9 @@ get-stream@^2.2.0:
pinkie-promise "^2.0.0"
graceful-fs@^4.1.10:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=
"graceful-readlink@>= 1.0.0":
version "1.0.1"
@@ -294,11 +287,6 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -312,9 +300,9 @@ once@^1.4.0:
wrappy "1"
opener@^1.4.3:
version "1.5.1"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==
version "1.4.3"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
integrity sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=
os-tmpdir@~1.0.2:
version "1.0.2"
@@ -379,9 +367,9 @@ seek-bzip@^1.0.5:
commander "~2.8.1"
semver@^5.3.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
"service-downloader@github:anthonydresser/service-downloader#0.1.5":
version "0.1.5"
@@ -409,16 +397,16 @@ strip-dirs@^2.0.0:
is-natural-number "^4.0.1"
tar-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
version "1.6.1"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.1.tgz#f84ef1696269d6223ca48f6e1eeede3f7e81f395"
integrity sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==
dependencies:
bl "^1.0.0"
buffer-alloc "^1.2.0"
buffer-alloc "^1.1.0"
end-of-stream "^1.0.0"
fs-constants "^1.0.0"
readable-stream "^2.3.0"
to-buffer "^1.1.1"
to-buffer "^1.1.0"
xtend "^4.0.0"
through@^2.3.6:
@@ -433,15 +421,15 @@ tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
to-buffer@^1.1.1:
to-buffer@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
unbzip2-stream@^1.0.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.1.tgz#7854da51622a7e63624221196357803b552966a1"
integrity sha512-fIZnvdjblYs7Cru/xC6tCPVhz7JkYcVQQkePwMLyQELzYTds2Xn8QefPVnvdVhhZqubxNA1cASXEH5wcK0Bucw==
version "1.2.5"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz#73a033a567bbbde59654b193c44d48a7e4f43c47"
integrity sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==
dependencies:
buffer "^3.0.1"
through "^2.3.6"

View File

@@ -2,7 +2,7 @@
"name": "profiler",
"displayName": "SQL Server Profiler",
"description": "SQL Server Profiler for Azure Data Studio",
"version": "0.5.0",
"version": "0.5.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "azuredatastudio",
"version": "1.3.3",
"version": "1.3.4",
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
"author": {
"name": "Microsoft Corporation"
@@ -68,7 +68,7 @@
"rxjs": "5.4.0",
"sanitize-html": "^1.19.1",
"semver": "^5.5.0",
"slickgrid": "github:anthonydresser/SlickGrid#2.3.29",
"slickgrid": "github:anthonydresser/SlickGrid#2.3.28",
"spdlog": "0.7.1",
"sudo-prompt": "8.2.0",
"svg.js": "^2.2.5",
@@ -102,12 +102,12 @@
"documentdb": "^1.5.1",
"electron-mksnapshot": "~1.7.0",
"eslint": "^3.4.0",
"event-stream": "^3.3.4",
"event-stream": "3.3.4",
"express": "^4.13.1",
"glob": "^5.0.13",
"gulp": "^3.9.1",
"gulp-atom-electron": "^1.16.1",
"gulp-azure-storage": "^0.7.0",
"gulp-atom-electron": "^1.19.2",
"gulp-azure-storage": "^0.8.2",
"gulp-bom": "^1.0.0",
"gulp-buffer": "0.0.2",
"gulp-cli": "^2.0.1",
@@ -120,7 +120,7 @@
"gulp-json-editor": "^2.2.1",
"gulp-mocha": "^2.1.3",
"gulp-plumber": "^1.2.0",
"gulp-remote-src": "^0.4.0",
"gulp-remote-src": "^0.4.4",
"gulp-rename": "^1.2.0",
"gulp-replace": "^0.5.4",
"gulp-shell": "^0.5.2",
@@ -129,7 +129,7 @@
"gulp-tslint": "^8.1.2",
"gulp-uglify": "^3.0.0",
"gulp-util": "^3.0.6",
"gulp-vinyl-zip": "^1.2.2",
"gulp-vinyl-zip": "^2.1.2",
"husky": "^0.13.1",
"innosetup-compiler": "^5.5.60",
"is": "^3.1.0",

View File

@@ -12,9 +12,10 @@ import * as types from 'vs/base/common/types';
import * as sqlops from 'sqlops';
export function appendRow(container: Builder, label: string, labelClass: string, cellContainerClass: string): Builder {
export function appendRow(container: Builder, label: string, labelClass: string, cellContainerClass: string, rowContainerClass?: string): Builder {
let cellContainer: Builder;
container.element('tr', {}, (rowContainer) => {
let rowAttributes = rowContainerClass ? { class: rowContainerClass } : {};
container.element('tr', rowAttributes, (rowContainer) => {
rowContainer.element('td', { class: labelClass }, (labelCellContainer) => {
labelCellContainer.div({}, (labelContainer) => {
labelContainer.text(label);

View File

@@ -75,7 +75,12 @@ export class SelectBox extends vsSelectBox {
// explicitly set the accessible role so that the screen readers can read the control type properly
this.selectElement.setAttribute('role', 'combobox');
this._selectBoxOptions = selectBoxOptions;
var focusTracker = dom.trackFocus(this.selectElement);
this._register(focusTracker);
this._register(focusTracker.onDidBlur(() => this._hideMessage()));
this._register(focusTracker.onDidFocus(() => this._showMessage()));
}
public style(styles: ISelectBoxStyles): void {
@@ -142,6 +147,10 @@ export class SelectBox extends vsSelectBox {
this.applyStyles();
}
public hasFocus(): boolean {
return document.activeElement === this.selectElement;
}
public showMessage(message: IMessage): void {
this.message = message;
@@ -163,7 +172,9 @@ export class SelectBox extends vsSelectBox {
aria.alert(alertText);
this._showMessage();
if (this.hasFocus()) {
this._showMessage();
}
}
public _showMessage(): void {

View File

@@ -9,27 +9,36 @@ export interface IObservableCollection<T> {
getLength(): number;
at(index: number): T;
getRange(start: number, end: number): T[];
setCollectionChangedCallback(callback: (startIndex: number, count: number) => void): void;
setLength(number): void;
setCollectionChangedCallback(callback: (change: CollectionChange, startIndex: number, count: number) => void): void;
dispose(): void;
}
export interface IGridDataRow {
row?: number;
values: any[];
}
export enum CollectionChange {
ItemsReplaced
}
class LoadCancellationToken {
isCancelled: boolean;
}
class DataWindow<T> {
private _data: T[];
class DataWindow<TData> {
private _data: TData[];
private _length: number = 0;
private _offsetFromDataSource: number = -1;
private lastLoadCancellationToken: LoadCancellationToken;
constructor(
private loadFunction: (offset: number, count: number) => Thenable<T[]>,
private placeholderItemGenerator: (index: number) => T,
private loadFunction: (offset: number, count: number) => Thenable<TData[]>,
private placeholderItemGenerator: (index: number) => TData,
private loadCompleteCallback: (start: number, end: number) => void
) { }
) {
}
dispose() {
this._data = undefined;
@@ -41,26 +50,26 @@ class DataWindow<T> {
}
}
public getStartIndex(): number {
getStartIndex(): number {
return this._offsetFromDataSource;
}
public getEndIndex(): number {
getEndIndex(): number {
return this._offsetFromDataSource + this._length;
}
public contains(dataSourceIndex: number): boolean {
contains(dataSourceIndex: number): boolean {
return dataSourceIndex >= this.getStartIndex() && dataSourceIndex < this.getEndIndex();
}
public getItem(index: number): T {
getItem(index: number): TData {
if (!this._data) {
return this.placeholderItemGenerator(index);
}
return this._data[index - this._offsetFromDataSource];
}
public positionWindow(offset: number, length: number): void {
positionWindow(offset: number, length: number): void {
this._offsetFromDataSource = offset;
this._length = length;
this._data = undefined;
@@ -83,28 +92,34 @@ class DataWindow<T> {
}
}
export class VirtualizedCollection<T extends Slick.SlickData> implements IObservableCollection<T> {
private _bufferWindowBefore: DataWindow<T>;
private _window: DataWindow<T>;
private _bufferWindowAfter: DataWindow<T>;
export class VirtualizedCollection<TData> implements IObservableCollection<TData> {
private collectionChangedCallback: (startIndex: number, count: number) => void;
private _length: number;
private _windowSize: number;
private _bufferWindowBefore: DataWindow<TData>;
private _window: DataWindow<TData>;
private _bufferWindowAfter: DataWindow<TData>;
private collectionChangedCallback: (change: CollectionChange, startIndex: number, count: number) => void;
constructor(
private readonly windowSize: number,
private placeHolderGenerator: (index: number) => T,
private length: number,
loadFn: (offset: number, count: number) => Thenable<T[]>
windowSize: number,
length: number,
loadFn: (offset: number, count: number) => Thenable<TData[]>,
private _placeHolderGenerator: (index: number) => TData
) {
this._windowSize = windowSize;
this._length = length;
let loadCompleteCallback = (start: number, end: number) => {
if (this.collectionChangedCallback) {
this.collectionChangedCallback(start, end - start);
this.collectionChangedCallback(CollectionChange.ItemsReplaced, start, end - start);
}
};
this._bufferWindowBefore = new DataWindow(loadFn, placeHolderGenerator, loadCompleteCallback);
this._window = new DataWindow(loadFn, placeHolderGenerator, loadCompleteCallback);
this._bufferWindowAfter = new DataWindow(loadFn, placeHolderGenerator, loadCompleteCallback);
this._bufferWindowBefore = new DataWindow(loadFn, _placeHolderGenerator, loadCompleteCallback);
this._window = new DataWindow(loadFn, _placeHolderGenerator, loadCompleteCallback);
this._bufferWindowAfter = new DataWindow(loadFn, _placeHolderGenerator, loadCompleteCallback);
}
dispose() {
@@ -113,23 +128,19 @@ export class VirtualizedCollection<T extends Slick.SlickData> implements IObserv
this._window.dispose();
}
public setCollectionChangedCallback(callback: (startIndex: number, count: number) => void): void {
setCollectionChangedCallback(callback: (change: CollectionChange, startIndex: number, count: number) => void): void {
this.collectionChangedCallback = callback;
}
public getLength(): number {
return this.length;
getLength(): number {
return this._length;
}
setLength(number: any): void {
this.length = number;
}
public at(index: number): T {
at(index: number): TData {
return this.getRange(index, index + 1)[0];
}
public getRange(start: number, end: number): T[] {
getRange(start: number, end: number): TData[] {
// current data may contain placeholders
let currentData = this.getRangeFromCurrent(start, end);
@@ -144,7 +155,7 @@ export class VirtualizedCollection<T extends Slick.SlickData> implements IObserv
this._bufferWindowAfter = this._window;
this._window = this._bufferWindowBefore;
this._bufferWindowBefore = windowToRecycle;
let newWindowOffset = Math.max(0, this._window.getStartIndex() - this.windowSize);
let newWindowOffset = Math.max(0, this._window.getStartIndex() - this._windowSize);
this._bufferWindowBefore.positionWindow(newWindowOffset, this._window.getStartIndex() - newWindowOffset);
} else if (start >= this._bufferWindowAfter.getStartIndex()) {
@@ -153,8 +164,8 @@ export class VirtualizedCollection<T extends Slick.SlickData> implements IObserv
this._bufferWindowBefore = this._window;
this._window = this._bufferWindowAfter;
this._bufferWindowAfter = windowToRecycle;
let newWindowOffset = Math.min(this._window.getStartIndex() + this.windowSize, this.length);
let newWindowLength = Math.min(this.length - newWindowOffset, this.windowSize);
let newWindowOffset = Math.min(this._window.getStartIndex() + this._windowSize, this._length);
let newWindowLength = Math.min(this._length - newWindowOffset, this._windowSize);
this._bufferWindowAfter.positionWindow(newWindowOffset, newWindowLength);
}
@@ -162,7 +173,7 @@ export class VirtualizedCollection<T extends Slick.SlickData> implements IObserv
return currentData;
}
private getRangeFromCurrent(start: number, end: number): T[] {
private getRangeFromCurrent(start: number, end: number): TData[] {
let currentData = [];
for (let i = 0; i < end - start; i++) {
currentData.push(this.getDataFromCurrent(start + i));
@@ -171,7 +182,7 @@ export class VirtualizedCollection<T extends Slick.SlickData> implements IObserv
return currentData;
}
private getDataFromCurrent(index: number): T {
private getDataFromCurrent(index: number): TData {
if (this._bufferWindowBefore.contains(index)) {
return this._bufferWindowBefore.getItem(index);
} else if (this._bufferWindowAfter.contains(index)) {
@@ -180,47 +191,39 @@ export class VirtualizedCollection<T extends Slick.SlickData> implements IObserv
return this._window.getItem(index);
}
return this.placeHolderGenerator(index);
return this._placeHolderGenerator(index);
}
private resetWindowsAroundIndex(index: number): void {
let bufferWindowBeforeStart = Math.max(0, index - this.windowSize * 1.5);
let bufferWindowBeforeEnd = Math.max(0, index - this.windowSize / 2);
let bufferWindowBeforeStart = Math.max(0, index - this._windowSize * 1.5);
let bufferWindowBeforeEnd = Math.max(0, index - this._windowSize / 2);
this._bufferWindowBefore.positionWindow(bufferWindowBeforeStart, bufferWindowBeforeEnd - bufferWindowBeforeStart);
let mainWindowStart = bufferWindowBeforeEnd;
let mainWindowEnd = Math.min(mainWindowStart + this.windowSize, this.length);
let mainWindowEnd = Math.min(mainWindowStart + this._windowSize, this._length);
this._window.positionWindow(mainWindowStart, mainWindowEnd - mainWindowStart);
let bufferWindowAfterStart = mainWindowEnd;
let bufferWindowAfterEnd = Math.min(bufferWindowAfterStart + this.windowSize, this.length);
let bufferWindowAfterEnd = Math.min(bufferWindowAfterStart + this._windowSize, this._length);
this._bufferWindowAfter.positionWindow(bufferWindowAfterStart, bufferWindowAfterEnd - bufferWindowAfterStart);
}
}
export class AsyncDataProvider<T extends Slick.SlickData> implements IDisposableDataProvider<T> {
export class AsyncDataProvider<TData extends IGridDataRow> implements IDisposableDataProvider<TData> {
constructor(public dataRows: IObservableCollection<T>) { }
constructor(private dataRows: IObservableCollection<TData>) { }
public getLength(): number {
return this.dataRows.getLength();
return this.dataRows ? this.dataRows.getLength() : 0;
}
public getItem(index: number): T {
return this.dataRows.at(index);
public getItem(index: number): TData {
return !this.dataRows ? undefined : this.dataRows.at(index);
}
public getRange(start: number, end: number): T[] {
return this.dataRows.getRange(start, end);
}
public set length(length: number) {
this.dataRows.setLength(length);
}
public get length(): number {
return this.dataRows.getLength();
public getRange(start: number, end: number): TData[] {
return !this.dataRows ? undefined : this.dataRows.getRange(start, end);
}
dispose() {

View File

@@ -5,6 +5,8 @@
'use strict';
import { range } from 'vs/base/common/arrays';
export interface IRowNumberColumnOptions {
numberOfRows: number;
cssClass?: string;
@@ -15,7 +17,7 @@ const sizePerDigit = 15;
export class RowNumberColumn<T> implements Slick.Plugin<T> {
private handler = new Slick.EventHandler();
private grid: Slick.Grid<T>;
private currentColumnWidth: number;
constructor(private options: IRowNumberColumnOptions) {
}
@@ -50,25 +52,19 @@ export class RowNumberColumn<T> implements Slick.Plugin<T> {
}
}
public updateRowCount(rowNum: number) {
this.options.numberOfRows = rowNum;
let columnWidth = Math.max(this.options.numberOfRows.toString().length * sizePerDigit, 22);
if (columnWidth !== this.currentColumnWidth) {
this.grid.setColumnWidths([this.getColumnDefinition()]);
}
}
public getColumnDefinition(): Slick.Column<T> {
// that smallest we can make it is 22 due to padding and margins in the cells
this.currentColumnWidth = Math.max(this.options.numberOfRows.toString().length * sizePerDigit, 22);
let columnWidth = Math.max(this.options.numberOfRows.toString().length * sizePerDigit, 22);
return {
id: 'rowNumber',
name: '',
field: 'rowNumber',
width: this.currentColumnWidth,
width: columnWidth,
minWidth: columnWidth,
maxWidth: columnWidth,
resizable: false,
cssClass: this.options.cssClass,
focusable: true,
focusable: false,
selectable: false,
formatter: (r, c, v, cd, dc) => this.formatter(r, c, v, cd, dc)
};

View File

@@ -12,7 +12,7 @@ import * as sqlops from 'sqlops';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
import { FirewallRuleDialog } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
import { IAccountManagementService, AzureResource } from 'sql/services/accountManagement/interfaces';
import { IResourceProviderService } from 'sql/parts/accountManagement/common/interfaces';
import { Deferred } from 'sql/base/common/promise';
@@ -61,7 +61,7 @@ export class FirewallRuleDialogController {
private handleOnCreateFirewallRule(): void {
let resourceProviderId = this._resourceProviderId;
this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount).then(tokenMappings => {
this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount, AzureResource.ResourceManagement).then(tokenMappings => {
let firewallRuleInfo: sqlops.FirewallRuleInfo = {
startIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.fromSubnetIPRange,
endIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.toSubnetIPRange,

View File

@@ -34,12 +34,13 @@ import { Deferred } from 'sql/base/common/promise';
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { values } from 'sql/base/common/objects';
import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
import { IAccountManagementService, AzureResource } from 'sql/services/accountManagement/interfaces';
import * as sqlops from 'sqlops';
import * as nls from 'vs/nls';
import * as errors from 'vs/base/common/errors';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import * as platform from 'vs/platform/registry/common/platform';
@@ -58,7 +59,6 @@ import * as statusbar from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { EditorGroup } from 'vs/workbench/common/editor/editorGroup';
export class ConnectionManagementService extends Disposable implements IConnectionManagementService {
@@ -100,7 +100,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti
@IStatusbarService private _statusBarService: IStatusbarService,
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
@IViewletService private _viewletService: IViewletService,
@IAngularEventingService private _angularEventing: IAngularEventingService
@IAngularEventingService private _angularEventing: IAngularEventingService,
@IAccountManagementService private _accountManagementService: IAccountManagementService
) {
super();
if (this._instantiationService) {
@@ -248,7 +249,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti
* Load the password for the profile
* @param connectionProfile Connection Profile
*/
public addSavedPassword(connectionProfile: IConnectionProfile): Promise<IConnectionProfile> {
public async addSavedPassword(connectionProfile: IConnectionProfile): Promise<IConnectionProfile> {
await this.fillInAzureTokenIfNeeded(connectionProfile);
return this._connectionStore.addSavedPassword(connectionProfile).then(result => result.profile);
}
@@ -274,7 +276,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
let self = this;
return new Promise<IConnectionResult>((resolve, reject) => {
// Load the password if it's not already loaded
self._connectionStore.addSavedPassword(connection).then(result => {
self._connectionStore.addSavedPassword(connection).then(async result => {
let newConnection = result.profile;
let foundPassword = result.savedCred;
@@ -286,8 +288,12 @@ export class ConnectionManagementService extends Disposable implements IConnecti
foundPassword = true;
}
}
// Fill in the Azure account token if needed and open the connection dialog if it fails
let tokenFillSuccess = await self.fillInAzureTokenIfNeeded(newConnection);
// If the password is required and still not loaded show the dialog
if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
if ((!foundPassword && self._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) || !tokenFillSuccess) {
resolve(self.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
} else {
// Try to connect
@@ -449,10 +455,14 @@ export class ConnectionManagementService extends Disposable implements IConnecti
showFirewallRuleOnError: true
};
}
return new Promise<IConnectionResult>((resolve, reject) => {
return new Promise<IConnectionResult>(async (resolve, reject) => {
if (callbacks.onConnectStart) {
callbacks.onConnectStart();
}
let tokenFillSuccess = await this.fillInAzureTokenIfNeeded(connection);
if (!tokenFillSuccess) {
throw new Error(nls.localize('connection.noAzureAccount', 'Failed to get Azure account token for connection'));
}
this.createNewConnection(uri, connection).then(connectionResult => {
if (connectionResult && connectionResult.connected) {
if (callbacks.onConnectSuccess) {
@@ -743,8 +753,33 @@ export class ConnectionManagementService extends Disposable implements IConnecti
}
}
private async fillInAzureTokenIfNeeded(connection: IConnectionProfile): Promise<boolean> {
if (connection.authenticationType !== Constants.azureMFA || connection.options['azureAccountToken']) {
return true;
}
let accounts = await this._accountManagementService.getAccountsForProvider('azurePublicCloud');
if (accounts && accounts.length > 0) {
let account = accounts.find(account => account.key.accountId === connection.userName);
if (account) {
if (account.isStale) {
try {
account = await this._accountManagementService.refreshAccount(account);
} catch {
// refreshAccount throws an error if the user cancels the dialog
return false;
}
}
let tokens = await this._accountManagementService.getSecurityToken(account, AzureResource.Sql);
connection.options['azureAccountToken'] = Object.values(tokens)[0].token;
connection.options['password'] = '';
return true;
}
}
return false;
}
// Request Senders
private sendConnectRequest(connection: IConnectionProfile, uri: string): Thenable<boolean> {
private async sendConnectRequest(connection: IConnectionProfile, uri: string): Promise<boolean> {
let connectionInfo = Object.assign({}, {
options: connection.options
});

View File

@@ -33,4 +33,5 @@ export const passwordChars = '***************';
/* authentication types */
export const sqlLogin = 'SqlLogin';
export const integrated = 'Integrated';
export const azureMFA = 'AzureMFA';

View File

@@ -22,6 +22,8 @@ import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
import { ConnectionProfile } from '../common/connectionProfile';
import * as styler from 'sql/common/theme/styler';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
import * as sqlops from 'sqlops';
@@ -30,7 +32,6 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView
import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import * as styler from 'vs/platform/theme/common/styler';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { Builder, $ } from 'vs/base/browser/builder';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
@@ -50,6 +51,11 @@ export class ConnectionWidget {
private _passwordInputBox: InputBox;
private _password: string;
private _rememberPasswordCheckBox: Checkbox;
private _azureAccountDropdown: SelectBox;
private _refreshCredentialsLinkBuilder: Builder;
private _addAzureAccountMessage: string = localize('connectionWidget.AddAzureAccount', 'Add an account...');
private readonly _azureProviderId = 'azurePublicCloud';
private _azureAccountList: sqlops.Account[];
private _advancedButton: Button;
private _callbacks: IConnectionComponentCallbacks;
private _authTypeSelectBox: SelectBox;
@@ -59,7 +65,7 @@ export class ConnectionWidget {
private _focusedBeforeHandleOnConnection: HTMLElement;
private _providerName: string;
private _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
[Constants.mssqlProviderName]: [new AuthenticationType(Constants.integrated, false), new AuthenticationType(Constants.sqlLogin, true)]
[Constants.mssqlProviderName]: [AuthenticationType.SqlLogin, AuthenticationType.Integrated, AuthenticationType.AzureMFA]
};
private _saveProfile: boolean;
private _databaseDropdownExpanded: boolean = false;
@@ -96,7 +102,8 @@ export class ConnectionWidget {
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@IClipboardService private _clipboardService: IClipboardService,
@IConfigurationService private _configurationService: IConfigurationService
@IConfigurationService private _configurationService: IConfigurationService,
@IAccountManagementService private _accountManagementService: IAccountManagementService
) {
this._callbacks = callbacks;
this._toDispose = [];
@@ -109,9 +116,9 @@ export class ConnectionWidget {
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
if (authTypeOption) {
if (OS === OperatingSystem.Windows) {
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.integrated);
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.Integrated);
} else {
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.sqlLogin);
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.SqlLogin);
}
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
}
@@ -182,7 +189,7 @@ export class ConnectionWidget {
// Username
let self = this;
let userNameOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
let userNameBuilder = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input');
let userNameBuilder = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input', 'username-password-row');
this._userNameInputBox = new InputBox(userNameBuilder.getHTMLElement(), this._contextViewService, {
validationOptions: {
validation: (value: string) => self.validateUsername(value, userNameOption.isRequired) ? ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', '{0} is required.', userNameOption.displayName) }) : null
@@ -191,14 +198,22 @@ export class ConnectionWidget {
});
// Password
let passwordOption = this._optionsMaps[ConnectionOptionSpecialType.password];
let passwordBuilder = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input');
let passwordBuilder = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input', 'username-password-row');
this._passwordInputBox = new InputBox(passwordBuilder.getHTMLElement(), this._contextViewService, { ariaLabel: passwordOption.displayName });
this._passwordInputBox.inputElement.type = 'password';
this._password = '';
// Remember password
let rememberPasswordLabel = localize('rememberPassword', 'Remember password');
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', false);
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', 'username-password-row', false);
// Azure account picker
let accountLabel = localize('connection.azureAccountDropdownLabel', 'Account');
let accountDropdownBuilder = DialogHelper.appendRow(this._tableContainer, accountLabel, 'connection-label', 'connection-input', 'azure-account-row');
this._azureAccountDropdown = new SelectBox([], undefined, this._contextViewService, accountDropdownBuilder.getContainer(), { ariaLabel: accountLabel });
DialogHelper.appendInputSelectBox(accountDropdownBuilder, this._azureAccountDropdown);
let refreshCredentialsBuilder = DialogHelper.appendRow(this._tableContainer, '', 'connection-label', 'connection-input', 'azure-account-row refresh-credentials-link');
this._refreshCredentialsLinkBuilder = refreshCredentialsBuilder.a({ href: '#' }).text(localize('connectionWidget.refreshAzureCredentials', 'Refresh account credentials'));
// Database
let databaseOption = this._optionsMaps[ConnectionOptionSpecialType.databaseName];
@@ -228,7 +243,7 @@ export class ConnectionWidget {
private validateUsername(value: string, isOptionRequired: boolean): boolean {
let currentAuthType = this._authTypeSelectBox ? this.getMatchingAuthType(this._authTypeSelectBox.value) : undefined;
if (!currentAuthType || currentAuthType.showUsernameAndPassword) {
if (!currentAuthType || currentAuthType === AuthenticationType.SqlLogin) {
if (!value && isOptionRequired) {
return true;
}
@@ -254,9 +269,9 @@ export class ConnectionWidget {
return button;
}
private appendCheckbox(container: Builder, label: string, checkboxClass: string, cellContainerClass: string, isChecked: boolean): Checkbox {
private appendCheckbox(container: Builder, label: string, checkboxClass: string, cellContainerClass: string, rowContainerClass: string, isChecked: boolean): Checkbox {
let checkbox: Checkbox;
container.element('tr', {}, (rowContainer) => {
container.element('tr', { class: rowContainerClass }, (rowContainer) => {
rowContainer.element('td');
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
checkbox = new Checkbox(inputCellContainer.getHTMLElement(), { label, checked: isChecked, ariaLabel: label });
@@ -275,6 +290,7 @@ export class ConnectionWidget {
this._toDispose.push(styler.attachSelectBoxStyler(this._serverGroupSelectBox, this._themeService));
this._toDispose.push(attachButtonStyler(this._advancedButton, this._themeService));
this._toDispose.push(attachCheckboxStyler(this._rememberPasswordCheckBox, this._themeService));
this._toDispose.push(styler.attachSelectBoxStyler(this._azureAccountDropdown, this._themeService));
if (this._authTypeSelectBox) {
// Theme styler
@@ -285,6 +301,23 @@ export class ConnectionWidget {
}));
}
if (this._azureAccountDropdown) {
this._toDispose.push(styler.attachSelectBoxStyler(this._azureAccountDropdown, this._themeService));
this._toDispose.push(this._azureAccountDropdown.onDidSelect(() => {
this.onAzureAccountSelected();
}));
}
if (this._refreshCredentialsLinkBuilder) {
this._toDispose.push(this._refreshCredentialsLinkBuilder.on(DOM.EventType.CLICK, async () => {
let account = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
if (account) {
await this._accountManagementService.refreshAccount(account);
this.fillInAzureAccountOptions();
}
}));
}
this._toDispose.push(this._serverGroupSelectBox.onDidSelect(selectedGroup => {
this.onGroupSelected(selectedGroup.selected);
}));
@@ -342,7 +375,7 @@ export class ConnectionWidget {
private setConnectButton(): void {
let showUsernameAndPassword: boolean = true;
if (this.authType) {
showUsernameAndPassword = this.authType.showUsernameAndPassword;
showUsernameAndPassword = this.authType === AuthenticationType.SqlLogin;
}
showUsernameAndPassword ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) :
this._callbacks.onSetConnectButton(!!this.serverName);
@@ -350,7 +383,7 @@ export class ConnectionWidget {
private onAuthTypeSelected(selectedAuthType: string) {
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
if (!currentAuthType.showUsernameAndPassword) {
if (currentAuthType !== AuthenticationType.SqlLogin) {
this._userNameInputBox.disable();
this._passwordInputBox.disable();
this._userNameInputBox.hideMessage();
@@ -366,6 +399,68 @@ export class ConnectionWidget {
this._passwordInputBox.enable();
this._rememberPasswordCheckBox.enabled = true;
}
if (currentAuthType === AuthenticationType.AzureMFA) {
this.fillInAzureAccountOptions();
this._azureAccountDropdown.enable();
let tableContainer = this._tableContainer.getContainer();
tableContainer.classList.add('hide-username-password');
tableContainer.classList.remove('hide-azure-accounts');
} else {
this._azureAccountDropdown.disable();
let tableContainer = this._tableContainer.getContainer();
tableContainer.classList.remove('hide-username-password');
tableContainer.classList.add('hide-azure-accounts');
this._azureAccountDropdown.hideMessage();
}
}
private async fillInAzureAccountOptions(): Promise<void> {
let oldSelection = this._azureAccountDropdown.value;
this._azureAccountList = await this._accountManagementService.getAccountsForProvider(this._azureProviderId);
let accountDropdownOptions = this._azureAccountList.map(account => account.key.accountId);
if (accountDropdownOptions.length === 0) {
// If there are no accounts add a blank option so that add account isn't automatically selected
accountDropdownOptions.unshift('');
}
accountDropdownOptions.push(this._addAzureAccountMessage);
this._azureAccountDropdown.setOptions(accountDropdownOptions);
this._azureAccountDropdown.selectWithOptionName(oldSelection);
this.updateRefreshCredentialsLink();
}
private async updateRefreshCredentialsLink(): Promise<void> {
let chosenAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
if (chosenAccount && chosenAccount.isStale) {
this._tableContainer.getContainer().classList.remove('hide-refresh-link');
} else {
this._tableContainer.getContainer().classList.add('hide-refresh-link');
}
}
private async onAzureAccountSelected(): Promise<void> {
// Reset the dropdown's validation message if the old selection was not valid but the new one is
this.validateAzureAccountSelection(false);
this._refreshCredentialsLinkBuilder.display('none');
// Open the add account dialog if needed, then select the added account
if (this._azureAccountDropdown.value === this._addAzureAccountMessage) {
let oldAccountIds = this._azureAccountList.map(account => account.key.accountId);
await this._accountManagementService.addAccount(this._azureProviderId);
// Refresh the dropdown's list to include the added account
await this.fillInAzureAccountOptions();
// If a new account was added find it and select it, otherwise select the first account
let newAccount = this._azureAccountList.find(option => !oldAccountIds.some(oldId => oldId === option.key.accountId));
if (newAccount) {
this._azureAccountDropdown.selectWithOptionName(newAccount.key.accountId);
} else {
this._azureAccountDropdown.select(0);
}
}
this.updateRefreshCredentialsLink();
}
private serverNameChanged(serverName: string) {
@@ -407,6 +502,7 @@ export class ConnectionWidget {
private clearValidationMessages(): void {
this._serverNameInputBox.hideMessage();
this._userNameInputBox.hideMessage();
this._azureAccountDropdown.hideMessage();
}
private getModelValue(value: string): string {
@@ -449,8 +545,8 @@ export class ConnectionWidget {
if (this._authTypeSelectBox) {
this.onAuthTypeSelected(this._authTypeSelectBox.value);
}
// Disable connect button if -
// 1. Authentication type is SQL Login and no username is provided
// 2. No server name is provided
@@ -513,7 +609,7 @@ export class ConnectionWidget {
currentAuthType = this.getMatchingAuthType(this._authTypeSelectBox.value);
}
if (!currentAuthType || currentAuthType.showUsernameAndPassword) {
if (!currentAuthType || currentAuthType === AuthenticationType.SqlLogin) {
this._userNameInputBox.enable();
this._passwordInputBox.enable();
this._rememberPasswordCheckBox.enabled = true;
@@ -537,7 +633,7 @@ export class ConnectionWidget {
}
public get userName(): string {
return this._userNameInputBox.value;
return this.authenticationType === AuthenticationType.AzureMFA ? this._azureAccountDropdown.value : this._userNameInputBox.value;
}
public get password(): string {
@@ -548,6 +644,27 @@ export class ConnectionWidget {
return this._authTypeSelectBox ? this.getAuthTypeName(this._authTypeSelectBox.value) : undefined;
}
private validateAzureAccountSelection(showMessage: boolean = true): boolean {
if (this.authType !== AuthenticationType.AzureMFA) {
return true;
}
let selected = this._azureAccountDropdown.value;
if (selected === '' || selected === this._addAzureAccountMessage) {
if (showMessage) {
this._azureAccountDropdown.showMessage({
content: localize('connectionWidget.invalidAzureAccount', 'You must select an account'),
type: MessageType.ERROR
});
}
return false;
} else {
this._azureAccountDropdown.hideMessage();
}
return true;
}
private validateInputs(): boolean {
let isFocused = false;
let validateServerName = this._serverNameInputBox.validate();
@@ -565,7 +682,12 @@ export class ConnectionWidget {
this._passwordInputBox.focus();
isFocused = true;
}
return validateServerName && validateUserName && validatePassword;
let validateAzureAccount = this.validateAzureAccountSelection();
if (!validateAzureAccount && !isFocused) {
this._azureAccountDropdown.focus();
isFocused = true;
}
return validateServerName && validateUserName && validatePassword && validateAzureAccount;
}
public connect(model: IConnectionProfile): boolean {
@@ -613,7 +735,7 @@ export class ConnectionWidget {
private getMatchingAuthType(displayName: string): AuthenticationType {
const authType = this._authTypeMap[this._providerName];
return authType ? authType.find(authType => this.getAuthTypeDisplayName(authType.name) === displayName) : undefined;
return authType ? authType.find(authType => this.getAuthTypeDisplayName(authType) === displayName) : undefined;
}
public closeDatabaseDropdown(): void {
@@ -634,18 +756,14 @@ export class ConnectionWidget {
}
private focusPasswordIfNeeded(): void {
if (this.authType && this.authType.showUsernameAndPassword && this.userName && !this.password) {
if (this.authType && this.authType === AuthenticationType.SqlLogin && this.userName && !this.password) {
this._passwordInputBox.focus();
}
}
}
class AuthenticationType {
public name: string;
public showUsernameAndPassword: boolean;
constructor(name: string, showUsernameAndPassword: boolean) {
this.name = name;
this.showUsernameAndPassword = showUsernameAndPassword;
}
enum AuthenticationType {
SqlLogin = 'SqlLogin',
Integrated = 'Integrated',
AzureMFA = 'AzureMFA'
}

View File

@@ -28,11 +28,12 @@
overflow: hidden;
margin: 0px 11px;
}
.connection-dialog .tabBody {
overflow: hidden;
flex: 1 1;
display: flex;
flex-direction: column;
flex: 1 1;
display: flex;
flex-direction: column;
}
.connection-recent, .connection-saved {
@@ -114,4 +115,16 @@
margin: 5px 0px;
padding: 5px 15px;
font-weight: 600;
}
}
.hide-azure-accounts .azure-account-row {
display: none;
}
.hide-username-password .username-password-row {
display: none;
}
.hide-refresh-link .azure-account-row.refresh-credentials-link {
display: none;
}

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $ } from 'vs/base/browser/dom';
import { escape } from 'sql/base/common/strings';
export class DBCellValue {
@@ -40,24 +41,20 @@ export function hyperLinkFormatter(row: number, cell: any, value: any, columnDef
*/
export function textFormatter(row: number, cell: any, value: any, columnDef: any, dataContext: any): string {
let cellClasses = 'grid-cell-value-container';
let valueToDisplay = '';
let titleValue = '';
let valueToDisplay: string = '';
if (DBCellValue.isDBCellValue(value)) {
valueToDisplay = 'NULL';
if (!value.isNull) {
valueToDisplay = value.displayValue.replace(/(\r\n|\n|\r)/g, ' ');
valueToDisplay = escape(valueToDisplay.length > 250 ? valueToDisplay.slice(0, 250) + '...' : valueToDisplay);
titleValue = value.displayValue;
valueToDisplay = escape(value.displayValue.replace(/(\r\n|\n|\r)/g, ' '));
} else {
cellClasses += ' missing-value';
}
} else if (typeof value === 'string') {
valueToDisplay = escape(value.length > 250 ? value.slice(0, 250) + '...' : value);
titleValue = value;
valueToDisplay = escape(value);
}
return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`;
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;
}
/** The following code is a rewrite over the both formatter function using dom builder

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularDisposable } from 'sql/base/common/lifecycle';
@@ -29,7 +29,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { RunCellAction, DeleteCellAction, AddCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
import { RunCellAction, CellContext, NotebookCellToggleMoreActon } from 'sql/parts/notebook/cellViews/codeActions';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
import { CellTypes } from 'sql/parts/notebook/models/contracts';
@@ -41,11 +41,12 @@ export const CODE_SELECTOR: string = 'code-component';
selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./code.component.html'))
})
export class CodeComponent extends AngularDisposable implements OnInit {
export class CodeComponent extends AngularDisposable implements OnInit, OnChanges {
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
@ViewChild('moreactions', { read: ElementRef }) private moreactionsElement: ElementRef;
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
@Input() cellModel: ICellModel;
@Input() hideVerticalToolbar: boolean = false;
@Output() public onContentChanged = new EventEmitter<void>();
@@ -53,14 +54,19 @@ export class CodeComponent extends AngularDisposable implements OnInit {
this._model = value;
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
protected _actionBar: Taskbar;
protected _moreActions: ActionBar;
private readonly _minimumHeight = 30;
private _editor: QueryTextEditor;
private _editorInput: UntitledEditorInput;
private _editorModel: ITextModel;
private _uri: string;
private _model: NotebookModel;
private _activeCellId: string;
private _toggleMoreActions: NotebookCellToggleMoreActon;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@@ -79,12 +85,26 @@ export class CodeComponent extends AngularDisposable implements OnInit {
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.initActionBar();
if (!this.hideVerticalToolbar) {
this.initActionBar();
}
}
ngOnChanges() {
ngAfterViewInit() {
this._toggleMoreActions = new NotebookCellToggleMoreActon(
this._instantiationService,
this.contextMenuService,
this.notificationService,
this.moreActionsElementRef,
this.model);
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
this.updateLanguageMode();
this.updateModel();
if (this._toggleMoreActions) {
this._toggleMoreActions.onChange(this.cellModel, changes);
}
}
ngAfterContentInit(): void {
@@ -98,6 +118,10 @@ export class CodeComponent extends AngularDisposable implements OnInit {
return this._model;
}
get activeCellId(): string {
return this._activeCellId;
}
private createEditor(): void {
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
this._editor = instantiationService.createInstance(QueryTextEditor);
@@ -139,21 +163,9 @@ export class CodeComponent extends AngularDisposable implements OnInit {
this._actionBar.setContent([
{ action: runCellAction }
]);
let moreActionsElement = <HTMLElement>this.moreactionsElement.nativeElement;
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
this._moreActions.context = { target: moreActionsElement };
let actions: Action[] = [];
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false));
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true));
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false));
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true));
actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')));
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, context), { icon: true, label: false });
}
private createUri(): URI {
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` });
// Use this to set the internal (immutable) and public (shared with extension) uri properties
@@ -180,7 +192,7 @@ export class CodeComponent extends AngularDisposable implements OnInit {
let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement;
toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
let moreactionsEl = <HTMLElement>this.moreactionsElement.nativeElement;
let moreactionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
moreactionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}

View File

@@ -11,7 +11,6 @@ code-component {
code-component .toolbar {
border-right-width: 1px;
border-right-style: solid;
}
code-component .toolbarIconRun {

View File

@@ -5,13 +5,20 @@
import { nb } from 'sqlops';
import { ElementRef, SimpleChange } from '@angular/core';
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import { localize } from 'vs/nls';
import { CellType } from 'sql/parts/notebook/models/contracts';
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { CellType, CellTypes } from 'sql/parts/notebook/models/contracts';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
import { getErrorMessage } from 'sql/parts/notebook/notebookUtils';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ICellModel, FutureInternal } from 'sql/parts/notebook/models/modelInterfaces';
import { ToggleableAction } from 'sql/parts/notebook/notebookActions';
@@ -173,7 +180,7 @@ export class DeleteCellAction extends CellActionBase {
runCellAction(context: CellContext): Promise<void> {
try {
context.model.deleteCell(context.cell);
context.model.deleteCell(context.cell);
} catch (error) {
let message = getErrorMessage(error);
@@ -184,4 +191,51 @@ export class DeleteCellAction extends CellActionBase {
}
return Promise.resolve();
}
}
export class NotebookCellToggleMoreActon {
private _actions: Action[] = [];
private _moreActions: ActionBar;
constructor (
private _instantiationService: IInstantiationService,
private contextMenuService: IContextMenuService,
private notificationService: INotificationService,
private moreActionElementRef: ElementRef,
private model: NotebookModel
) {
this._actions.push(
this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false, this.notificationService),
this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true, this.notificationService),
this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false, this.notificationService),
this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true, this.notificationService),
this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'), this.notificationService)
);
let moreActionsElement = <HTMLElement>this.moreActionElementRef.nativeElement;
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
this._moreActions.context = { target: moreActionsElement };
}
toggle(showIcon: boolean): void {
if (showIcon) {
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, this.model, this.contextMenuService), { icon: showIcon, label: false });
} else if (this._moreActions) {
this._moreActions.clear();
}
}
public onChange(cellModel: ICellModel, changes: { [propKey: string]: SimpleChange }): void {
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
if (cellModel.id === changedProp.currentValue) {
this.toggle(true);
}
else {
this.toggle(false);
}
break;
}
}
}
}

View File

@@ -6,7 +6,7 @@
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="notebook-code" style="flex: 0 0 auto;">
<code-component [cellModel]="cellModel" [model]="model"></code-component>
<code-component [cellModel]="cellModel" [model]="model" [activeCellId]="activeCellId"></code-component>
</div>
<div #codeCellOutput class="notebook-output" style="flex: 0 0 auto;">
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel">

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./codeCell';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, SimpleChange, OnChanges } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
@@ -21,13 +21,19 @@ export const CODE_SELECTOR: string = 'code-cell-component';
selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./codeCell.component.html'))
})
export class CodeCellComponent extends CellView implements OnInit {
export class CodeCellComponent extends CellView implements OnInit, OnChanges {
@ViewChild('codeCellOutput', { read: ElementRef }) private outputPreview: ElementRef;
private _model: NotebookModel;
@Input() cellModel: ICellModel;
@Input() set model(value: NotebookModel) {
this._model = value;
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
private _model: NotebookModel;
private _activeCellId: string;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@@ -46,6 +52,24 @@ export class CodeCellComponent extends CellView implements OnInit {
}
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
break;
}
}
}
get model(): NotebookModel {
return this._model;
}
get activeCellId(): string {
return this._activeCellId;
}
// Todo: implement layout
public layout() {
@@ -56,8 +80,4 @@ export class CodeCellComponent extends CellView implements OnInit {
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
get model(): NotebookModel {
return this._model;
}
}

View File

@@ -6,7 +6,7 @@
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="notebook-text" style="flex: 0 0 auto;">
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()"></code-component>
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [activeCellId]="activeCellId" [hideVerticalToolbar]=1></code-component>
</div>
<div #preview class="notebook-preview" style="flex: 0 0 auto;" (dblclick)="toggleEditMode()">
</div>

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./textCell';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
@@ -22,12 +22,16 @@ export const TEXT_SELECTOR: string = 'text-cell-component';
selector: TEXT_SELECTOR,
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
})
export class TextCellComponent extends CellView implements OnInit {
export class TextCellComponent extends CellView implements OnInit, OnChanges {
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
@Input() cellModel: ICellModel;
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
private _content: string;
private isEditMode: boolean;
private _sanitizer: ISanitizer;
private _activeCellId: string;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@@ -39,8 +43,15 @@ export class TextCellComponent extends CellView implements OnInit {
this.isEditMode = false;
}
ngOnChanges() {
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
this.updatePreview();
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
break;
}
}
}
//Gets sanitizer from ISanitizer interface
@@ -51,6 +62,10 @@ export class TextCellComponent extends CellView implements OnInit {
return this._sanitizer = defaultSanitizer;
}
get activeCellId(): string {
return this._activeCellId;
}
/**
* Updates the preview of markdown component with latest changes
* If content is empty and in non-edit mode, default it to 'Double-click to edit'

View File

@@ -10,9 +10,9 @@
<div class="scrollable" style="flex: 1 1 auto; position: relative">
<loading-spinner [loading]="isLoading"></loading-spinner>
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" (keydown)="onKeyDown($event)">
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model">
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell">
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [activeCellId]="activeCellId">
</text-cell-component>
</div>
</div>

View File

@@ -54,6 +54,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
private _modelRegisteredDeferred = new Deferred<NotebookModel>();
private profile: IConnectionProfile;
private _trustedAction: TrustedAction;
private _activeCellId: string;
constructor(
@@ -85,6 +86,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
return this._model;
}
public get activeCellId(): string {
return this._activeCellId;
}
public get modelRegistered(): Promise<NotebookModel> {
return this._modelRegisteredDeferred.promise;
}
@@ -106,6 +111,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
this._activeCell = cell;
this._activeCell.active = true;
this._model.activeCell = this._activeCell;
this._activeCellId = cell.id;
this._changeRef.detectChanges();
}
}

View File

@@ -261,7 +261,7 @@ export class ObjectExplorerService implements IObjectExplorerService {
return this._activeObjectExplorerNodes[connection.id];
}
public createNewSession(providerId: string, connection: ConnectionProfile): Thenable<sqlops.ObjectExplorerSessionResponse> {
public async createNewSession(providerId: string, connection: ConnectionProfile): Promise<sqlops.ObjectExplorerSessionResponse> {
let self = this;
return new Promise<sqlops.ObjectExplorerSessionResponse>((resolve, reject) => {
let provider = this._providers[providerId];

View File

@@ -29,7 +29,7 @@ export interface IQueryManagementService {
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>;
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>;
@@ -38,8 +38,7 @@ export interface IQueryManagementService {
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;
onResultSetComplete(resultSetInfo: sqlops.QueryExecuteResultSetCompleteNotificationParams): void;
onMessage(message: sqlops.QueryExecuteMessageParams): void;
// Edit Data Callbacks
@@ -66,7 +65,7 @@ export interface IQueryRequestHandler {
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>;
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>;
@@ -245,15 +244,9 @@ export class QueryManagementService implements IQueryManagementService {
});
}
public onResultSetAvailable(resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
public onResultSetComplete(resultSetInfo: sqlops.QueryExecuteResultSetCompleteNotificationParams): 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);
runner.handleResultSetComplete(resultSetInfo);
});
}

View File

@@ -4,8 +4,6 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as pretty from 'pretty-data';
import { attachTableStyler } from 'sql/common/theme/styler';
import QueryRunner from 'sql/parts/query/execution/queryRunner';
import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView';
@@ -14,7 +12,7 @@ import { ScrollableSplitView } from 'sql/base/browser/ui/scrollableSplitview/scr
import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, RestoreTableAction, ChartDataAction } from 'sql/parts/query/editor/actions';
import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, RestoreTableAction, ChartDataAction, ShowQueryPlanAction } from 'sql/parts/query/editor/actions';
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
import { escape } from 'sql/base/common/strings';
@@ -22,9 +20,9 @@ import { hyperLinkFormatter, textFormatter } from 'sql/parts/grid/services/share
import { CopyKeybind } from 'sql/base/browser/ui/table/plugins/copyKeybind.plugin';
import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin';
import { ITableStyles, ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces';
import { warn } from 'sql/base/common/log';
import * as sqlops from 'sqlops';
import * as pretty from 'pretty-data';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
@@ -40,7 +38,7 @@ import { $ } from 'vs/base/browser/builder';
import { generateUuid } from 'vs/base/common/uuid';
import { TPromise } from 'vs/base/common/winjs.base';
import { Separator, ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { isInDOM } from 'vs/base/browser/dom';
import { Dimension, getContentWidth, isInDOM } from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -177,8 +175,7 @@ export class GridPanel extends ViewletPanel {
this.reset();
this.queryRunnerDisposables = [];
this.runner = runner;
this.queryRunnerDisposables.push(this.runner.onResultSet(this.onResultSet, this));
this.queryRunnerDisposables.push(this.runner.onResultSetUpdate(this.updateResultSet, this));
this.queryRunnerDisposables.push(this.runner.onResultSet(e => this.onResultSet(e)));
this.queryRunnerDisposables.push(this.runner.onQueryStart(() => {
if (this.state) {
this.state.tableStates = [];
@@ -203,33 +200,6 @@ export class GridPanel extends ViewletPanel {
}
}
private updateResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
let resultsToUpdate: sqlops.ResultSetSummary[];
if (!Array.isArray(resultSet)) {
resultsToUpdate = [resultSet];
} else {
resultsToUpdate = resultSet;
}
for (let set of resultsToUpdate) {
let table = this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id);
if (table) {
table.updateResult(set);
} else {
warn('Got result set update request for non-existant table');
}
}
this.maximumBodySize = this.tables.reduce((p, c) => {
return p + c.maximumSize;
}, 0);
if (this.state && this.state.scrollPosition) {
this.splitView.setScrollPosition(this.state.scrollPosition);
}
}
private addResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
let resultsToAdd: sqlops.ResultSetSummary[];
if (!Array.isArray(resultSet)) {
@@ -353,12 +323,9 @@ class GridTable<T> extends Disposable implements IView {
private selectionModel = new CellSelectionModel();
private styles: ITableStyles;
private currentHeight: number;
private dataProvider: AsyncDataProvider<T>;
private columns: Slick.Column<T>[];
private rowNumberColumn: RowNumberColumn<T>;
private _onDidChange = new Emitter<number>();
public readonly onDidChange: Event<number> = this._onDidChange.event;
@@ -368,20 +335,13 @@ class GridTable<T> extends Disposable implements IView {
private _state: GridTableState;
private scrolled = false;
private visible = false;
public get resultSet(): sqlops.ResultSetSummary {
return this._resultSet;
}
// this handles if the row count is small, like 4-5 rows
private get maxSize(): number {
return ((this.resultSet.rowCount) * ROW_HEIGHT) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT;
}
private readonly maxSize = ((this.resultSet.rowCount) * ROW_HEIGHT) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT;
constructor(
private runner: QueryRunner,
private _resultSet: sqlops.ResultSetSummary,
public readonly resultSet: sqlops.ResultSetSummary,
@IContextMenuService private contextMenuService: IContextMenuService,
@IInstantiationService private instantiationService: IInstantiationService,
@IEditorService private editorService: IEditorService,
@@ -407,31 +367,7 @@ class GridTable<T> extends Disposable implements IView {
});
}
public onAdd() {
this.visible = true;
let collection = new VirtualizedCollection(
50,
index => this.placeholdGenerator(index),
this.resultSet.rowCount,
(offset, count) => this.loadData(offset, count)
);
collection.setCollectionChangedCallback((startIndex, count) => {
this.renderGridDataRowsRange(startIndex, count);
});
this.dataProvider.dataRows = collection;
this.table.updateRowCount();
}
public onRemove() {
this.visible = false;
let collection = new VirtualizedCollection(
50,
index => this.placeholdGenerator(index),
0,
() => TPromise.as([])
);
this.dataProvider.dataRows = collection;
this.table.updateRowCount();
// when we are removed slickgrid acts badly so we need to account for that
this.scrolled = false;
}
@@ -443,38 +379,34 @@ class GridTable<T> extends Disposable implements IView {
private build(): void {
let tableContainer = document.createElement('div');
tableContainer.style.display = 'inline-block';
tableContainer.style.width = `calc(100% - ${ACTIONBAR_WIDTH}px)`;
this.container.appendChild(tableContainer);
let collection = new VirtualizedCollection(
50,
index => this.placeholdGenerator(index),
0,
() => TPromise.as([])
let collection = new VirtualizedCollection(50, this.resultSet.rowCount,
(offset, count) => this.loadData(offset, count),
index => this.placeholdGenerator(index)
);
collection.setCollectionChangedCallback((startIndex, count) => {
collection.setCollectionChangedCallback((change, startIndex, count) => {
this.renderGridDataRowsRange(startIndex, count);
});
this.rowNumberColumn = new RowNumberColumn({ numberOfRows: this.resultSet.rowCount });
let numberColumn = new RowNumberColumn({ numberOfRows: this.resultSet.rowCount });
let copyHandler = new CopyKeybind();
copyHandler.onCopy(e => {
new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false).run(this.generateContext());
});
this.columns.unshift(this.rowNumberColumn.getColumnDefinition());
this.columns.unshift(numberColumn.getColumnDefinition());
let tableOptions: Slick.GridOptions<T> = {
rowHeight: ROW_HEIGHT,
showRowNumber: true,
forceFitColumns: false,
defaultColumnWidth: 120
};
this.dataProvider = new AsyncDataProvider(collection);
this.table = this._register(new Table(tableContainer, { dataProvider: this.dataProvider, columns: this.columns }, tableOptions));
this.table = this._register(new Table(tableContainer, { dataProvider: new AsyncDataProvider(collection), columns: this.columns }, tableOptions));
this.table.setSelectionModel(this.selectionModel);
this.table.registerPlugin(new MouseWheelSupport());
this.table.registerPlugin(new AutoColumnSize());
this.table.registerPlugin(copyHandler);
this.table.registerPlugin(this.rowNumberColumn);
this.table.registerPlugin(numberColumn);
this.table.registerPlugin(new AdditionalKeyBindings());
this._register(this.table.onContextMenu(this.contextMenu, this));
this._register(this.table.onClick(this.onTableClick, this));
@@ -575,7 +507,7 @@ class GridTable<T> extends Disposable implements IView {
} catch (e) {
// If Xml fails to parse, fall back on original Xml content
}
} else {
} else {
let jsonContent: string = undefined;
try {
jsonContent = JSON.parse(content);
@@ -594,15 +526,6 @@ class GridTable<T> extends Disposable implements IView {
}
}
public updateResult(resultSet: sqlops.ResultSetSummary) {
this._resultSet = resultSet;
if (this.table && this.visible) {
this.dataProvider.length = resultSet.rowCount;
this.table.updateRowCount();
}
this.rowNumberColumn.updateRowCount(resultSet.rowCount);
}
private generateContext(cell?: Slick.Cell): IGridActionContext {
const selection = this.selectionModel.getSelectedRanges();
return <IGridActionContext>{
@@ -654,7 +577,12 @@ class GridTable<T> extends Disposable implements IView {
} else {
this.currentHeight = size;
}
this.table.layout(size, Orientation.VERTICAL);
this.table.layout(
new Dimension(
getContentWidth(this.container) - ACTIONBAR_WIDTH,
size
)
);
}
public get minimumSize(): number {
@@ -668,9 +596,6 @@ class GridTable<T> extends Disposable implements IView {
private loadData(offset: number, count: number): Thenable<T[]> {
return this.runner.getQueryRows(offset, count, this.resultSet.batchId, this.resultSet.id).then(response => {
if (!response.resultSubset) {
return [];
}
return response.resultSubset.rows.map(r => {
let dataWithSchema = {};
// skip the first column since its a number column

View File

@@ -100,18 +100,6 @@ export default class QueryRunner extends Disposable {
private _echoedResultSet = echo(this._debouncedResultSet.event);
public readonly onResultSet = this._echoedResultSet.event;
private _onResultSetUpdate = this._register(new Emitter<sqlops.ResultSetSummary>());
private _debouncedResultSetUpdate = debounceEvent<sqlops.ResultSetSummary, sqlops.ResultSetSummary[]>(this._onResultSetUpdate.event, (l, e) => {
// on first run
if (types.isUndefinedOrNull(l)) {
return [e];
} else {
return l.concat(e);
}
});
private _echoedResultSetUpdate = echo(this._debouncedResultSetUpdate.event);
public readonly onResultSetUpdate = this._echoedResultSetUpdate.event;
private _onQueryStart = this._register(new Emitter<void>());
public readonly onQueryStart: Event<void> = this._onQueryStart.event;
@@ -348,7 +336,7 @@ export default class QueryRunner extends Disposable {
/**
* Handle a ResultSetComplete from the service layer
*/
public handleResultSetAvailable(result: sqlops.QueryExecuteResultSetNotificationParams): void {
public handleResultSetComplete(result: sqlops.QueryExecuteResultSetCompleteNotificationParams): void {
if (result && result.resultSetSummary) {
let resultSet = result.resultSetSummary;
let batchSet: sqlops.BatchSummary;
@@ -386,27 +374,6 @@ export default class QueryRunner extends Disposable {
}
}
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
*/

View File

@@ -249,9 +249,9 @@ export class QueryEditorService implements IQueryEditorService {
QueryEditorService.editorService.openEditor(newEditorInput, options, group).then((editor) => {
resolve(QueryEditorService._onEditorOpened(editor, uri.toString(), undefined, options.pinned));
},
(error) => {
reject(error);
});
(error) => {
reject(error);
});
});
});
}

View File

@@ -20,7 +20,7 @@ import { AccountDialogController } from 'sql/parts/accountManagement/accountDial
import { AutoOAuthDialogController } from 'sql/parts/accountManagement/autoOAuthDialog/autoOAuthDialogController';
import { AccountListStatusbarItem } from 'sql/parts/accountManagement/accountListStatusbar/accountListStatusbarItem';
import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/services/accountManagement/eventTypes';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
import { IAccountManagementService, AzureResource } from 'sql/services/accountManagement/interfaces';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
export class AccountManagementService implements IAccountManagementService {
@@ -217,11 +217,12 @@ export class AccountManagementService implements IAccountManagementService {
/**
* Generates a security token by asking the account's provider
* @param {Account} account Account to generate security token for
* @param {AzureResource} resource The resource to get the security token for
* @return {Thenable<{}>} Promise to return the security token
*/
public getSecurityToken(account: sqlops.Account): Thenable<{}> {
public getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}> {
return this.doWithProvider(account.key.providerId, provider => {
return provider.provider.getSecurityToken(account);
return provider.provider.getSecurityToken(account, resource);
});
}

View File

@@ -22,7 +22,7 @@ export interface IAccountManagementService {
addAccount(providerId: string): Thenable<void>;
getAccountProviderMetadata(): Thenable<sqlops.AccountProviderMetadata[]>;
getAccountsForProvider(providerId: string): Thenable<sqlops.Account[]>;
getSecurityToken(account: sqlops.Account): Thenable<{}>;
getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}>;
removeAccount(accountKey: sqlops.AccountKey): Thenable<boolean>;
refreshAccount(account: sqlops.Account): Thenable<sqlops.Account>;
@@ -44,6 +44,12 @@ export interface IAccountManagementService {
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

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* 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/parts/connection/common/connectionManagement';
import { TPromise } from 'vs/base/common/winjs.base';
import { localize } from 'vs/nls';
import { data } from 'vs/base/test/common/filters.perf.data';
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")));
}
}
}

64
src/sql/sqlops.d.ts vendored
View File

@@ -37,6 +37,8 @@ declare module 'sqlops' {
export function registerCapabilitiesServiceProvider(provider: CapabilitiesProvider): vscode.Disposable;
export function registerDacFxServicesProvider(provider: DacFxServicesProvider): vscode.Disposable;
/**
* An [event](#Event) which fires when the specific flavor of a language used in DMP
* connections has changed. And example is for a SQL connection, the flavor changes
@@ -692,8 +694,7 @@ declare module 'sqlops' {
registerOnQueryComplete(handler: (result: QueryExecuteCompleteNotificationResult) => any): void;
registerOnBatchStart(handler: (batchInfo: QueryExecuteBatchNotificationParams) => any): void;
registerOnBatchComplete(handler: (batchInfo: QueryExecuteBatchNotificationParams) => any): void;
registerOnResultSetAvailable(handler: (resultSetInfo: QueryExecuteResultSetNotificationParams) => any): void;
registerOnResultSetUpdated(handler: (resultSetInfo: QueryExecuteResultSetNotificationParams) => any): void;
registerOnResultSetComplete(handler: (resultSetInfo: QueryExecuteResultSetCompleteNotificationParams) => any): void;
registerOnMessage(handler: (message: QueryExecuteMessageParams) => any): void;
// Edit Data Requests
@@ -768,7 +769,6 @@ declare module 'sqlops' {
batchId: number;
rowCount: number;
columnInfo: IDbColumn[];
complete: boolean;
}
export interface BatchSummary {
@@ -837,7 +837,7 @@ declare module 'sqlops' {
}
export interface QueryExecuteResultSetNotificationParams {
export interface QueryExecuteResultSetCompleteNotificationParams {
resultSetSummary: ResultSetSummary;
ownerUri: string;
}
@@ -1585,6 +1585,49 @@ declare module 'sqlops' {
registerOnUpdated(handler: () => any): void;
}
// DacFx interfaces -----------------------------------------------------------------------
export interface DacFxResult extends ResultStatus {
operationId: string;
}
export interface ExportParams {
databaseName: string;
packageFilePath: string;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export interface ImportParams {
packageFilePath: string;
databaseName: string;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export interface ExtractParams {
databaseName: string;
packageFilePath: string;
applicationName: string;
applicationVersion: string;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export interface DeployParams {
packageFilePath: string;
databaseName: string;
upgradeExisting: boolean;
ownerUri: string;
taskExecutionMode: TaskExecutionMode;
}
export interface DacFxServicesProvider extends DataProvider {
exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable<DacFxResult>;
importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable<DacFxResult>;
extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable<DacFxResult>;
deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable<DacFxResult>;
}
// Security service interfaces ------------------------------------------------------------------------
export interface CredentialInfo {
id: number;
@@ -1914,10 +1957,11 @@ declare module 'sqlops' {
/**
* Generates a security token by asking the account's provider
* @param {Account} account Account to generate security token for
* @param {Account} account Account to generate security token for (defaults to
* AzureResource.ResourceManagement if not given)
* @return {Thenable<{}>} Promise to return the security token
*/
export function getSecurityToken(account: Account): Thenable<{}>;
export function getSecurityToken(account: Account, resource?: AzureResource): Thenable<{}>;
/**
* An [event](#Event) which fires when the accounts have changed.
@@ -1990,6 +2034,11 @@ declare module 'sqlops' {
isStale: boolean;
}
export enum AzureResource {
ResourceManagement = 0,
Sql = 1
}
export interface DidChangeAccountsParams {
// Updated accounts
accounts: Account[];
@@ -2047,9 +2096,10 @@ declare module 'sqlops' {
/**
* Generates a security token for the provided account
* @param {Account} account The account to generate a security token for
* @param {AzureResource} resource The resource to get the token for
* @return {Thenable<{}>} Promise to return a security token object
*/
getSecurityToken(account: Account): Thenable<{}>;
getSecurityToken(account: Account, resource: AzureResource): Thenable<{}>;
/**
* Prompts the user to enter account information.

View File

@@ -1232,7 +1232,8 @@ declare module 'sqlops' {
QueryProvider = 'QueryProvider',
AdminServicesProvider = 'AdminServicesProvider',
AgentServicesProvider = 'AgentServicesProvider',
CapabilitiesProvider = 'CapabilitiesProvider'
CapabilitiesProvider = 'CapabilitiesProvider',
DacFxServicesProvider = 'DacFxServicesProvider',
}
export namespace dataprotocol {

View File

@@ -285,7 +285,8 @@ export enum DataProviderType {
QueryProvider = 'QueryProvider',
AdminServicesProvider = 'AdminServicesProvider',
AgentServicesProvider = 'AgentServicesProvider',
CapabilitiesProvider = 'CapabilitiesProvider'
CapabilitiesProvider = 'CapabilitiesProvider',
DacFxServicesProvider = 'DacFxServicesProvider',
}
export enum DeclarativeDataType {
@@ -313,6 +314,11 @@ export class TreeComponentItem extends TreeItem {
checked?: boolean;
}
export enum AzureResource {
ResourceManagement = 0,
Sql = 1
}
export class SqlThemeIcon {
static readonly Folder = new SqlThemeIcon('Folder');

View File

@@ -13,6 +13,7 @@ import {
MainThreadAccountManagementShape,
SqlMainContext,
} from 'sql/workbench/api/node/sqlExtHost.protocol';
import { AzureResource } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
import { Event, Emitter } from 'vs/base/common/event';
@@ -89,12 +90,15 @@ export class ExtHostAccountManagement extends ExtHostAccountManagementShape {
return Promise.all(promises).then(() => resultAccounts);
}
public $getSecurityToken(account: sqlops.Account): Thenable<{}> {
public $getSecurityToken(account: sqlops.Account, resource?: sqlops.AzureResource): Thenable<{}> {
if (resource === undefined) {
resource = AzureResource.ResourceManagement;
}
return this.$getAllAccounts().then(() => {
for (const handle in this._accounts) {
const providerHandle = parseInt(handle);
if (this._accounts[handle].findIndex((acct) => acct.key.accountId === account.key.accountId) !== -1) {
return this._withProvider(providerHandle, (provider: sqlops.AccountProvider) => provider.getSecurityToken(account));
return this._withProvider(providerHandle, (provider: sqlops.AccountProvider) => provider.getSecurityToken(account, resource));
}
}

View File

@@ -156,6 +156,12 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
return rt;
}
$registerDacFxServiceProvider(provider: sqlops.DacFxServicesProvider): vscode.Disposable {
let rt = this.registerProvider(provider, DataProviderType.DacFxServicesProvider);
this._proxy.$registerDacFxServicesProvider(provider.providerId, provider.handle);
return rt;
}
// Capabilities Discovery handlers
$getServerCapabilities(handle: number, client: sqlops.DataProtocolClientCapabilities): Thenable<sqlops.DataProtocolServerCapabilities> {
return this._resolveProvider<sqlops.CapabilitiesProvider>(handle).getServerCapabilities(client);
@@ -259,11 +265,8 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
$onBatchComplete(handle: number, batchInfo: sqlops.QueryExecuteBatchNotificationParams): void {
this._proxy.$onBatchComplete(handle, batchInfo);
}
$onResultSetAvailable(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._proxy.$onResultSetAvailable(handle, resultSetInfo);
}
$onResultSetUpdated(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._proxy.$onResultSetUpdated(handle, resultSetInfo);
$onResultSetComplete(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetCompleteNotificationParams): void {
this._proxy.$onResultSetComplete(handle, resultSetInfo);
}
$onQueryMessage(handle: number, message: sqlops.QueryExecuteMessageParams): void {
this._proxy.$onQueryMessage(handle, message);

View File

@@ -76,8 +76,8 @@ export class MainThreadAccountManagement implements MainThreadAccountManagementS
clear(accountKey: sqlops.AccountKey): Thenable<void> {
return self._proxy.$clear(handle, accountKey);
},
getSecurityToken(account: sqlops.Account): Thenable<{}> {
return self._proxy.$getSecurityToken(account);
getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}> {
return self._proxy.$getSecurityToken(account, resource);
},
initialize(restoredAccounts: sqlops.Account[]): Thenable<sqlops.Account[]> {
return self._proxy.$initialize(handle, restoredAccounts);

View File

@@ -26,6 +26,7 @@ import { IProfilerService } from 'sql/parts/profiler/service/interfaces';
import { ISerializationService } from 'sql/services/serialization/serializationService';
import { IFileBrowserService } from 'sql/parts/fileBrowser/common/interfaces';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { IDacFxService } from 'sql/services/dacfx/dacFxService';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
/**
@@ -55,7 +56,8 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
@ITaskService private _taskService: ITaskService,
@IProfilerService private _profilerService: IProfilerService,
@ISerializationService private _serializationService: ISerializationService,
@IFileBrowserService private _fileBrowserService: IFileBrowserService
@IFileBrowserService private _fileBrowserService: IFileBrowserService,
@IDacFxService private _dacFxService: IDacFxService,
) {
if (extHostContext) {
this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostDataProtocol);
@@ -399,6 +401,26 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
return undefined;
}
public $registerDacFxServicesProvider(providerId: string, handle: number): TPromise<any> {
const self = this;
this._dacFxService.registerProvider(providerId, <sqlops.DacFxServicesProvider>{
exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return self._proxy.$exportBacpac(handle, databaseName, packageFilePath, ownerUri, taskExecutionMode);
},
importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return self._proxy.$importBacpac(handle, packageFilePath, databaseName, ownerUri, taskExecutionMode);
},
extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return self._proxy.$extractDacpac(handle, databaseName, packageFilePath, applicationName, applicationVersion, ownerUri, taskExecutionMode);
},
deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> {
return self._proxy.$deployDacpac(handle, packageFilePath, databaseName, upgradeExisting, ownerUri, taskExecutionMode);
}
});
return undefined;
}
// Connection Management handlers
public $onConnectionComplete(handle: number, connectionInfoSummary: sqlops.ConnectionInfoSummary): void {
this._connectionManagementService.onConnectionComplete(handle, connectionInfoSummary);
@@ -422,11 +444,8 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
public $onBatchComplete(handle: number, batchInfo: sqlops.QueryExecuteBatchNotificationParams): void {
this._queryManagementService.onBatchComplete(batchInfo);
}
public $onResultSetAvailable(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._queryManagementService.onResultSetAvailable(resultSetInfo);
}
public $onResultSetUpdated(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void {
this._queryManagementService.onResultSetUpdated(resultSetInfo);
public $onResultSetComplete(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetCompleteNotificationParams): void {
this._queryManagementService.onResultSetComplete(resultSetInfo);
}
public $onQueryMessage(handle: number, message: sqlops.QueryExecuteMessageParams): void {
this._queryManagementService.onMessage(message);

View File

@@ -97,8 +97,8 @@ export function createApiFactory(
getAllAccounts(): Thenable<sqlops.Account[]> {
return extHostAccountManagement.$getAllAccounts();
},
getSecurityToken(account: sqlops.Account): Thenable<{}> {
return extHostAccountManagement.$getSecurityToken(account);
getSecurityToken(account: sqlops.Account, resource?: sqlops.AzureResource): Thenable<{}> {
return extHostAccountManagement.$getSecurityToken(account, resource);
},
onDidChangeAccounts(listener: (e: sqlops.DidChangeAccountsParams) => void, thisArgs?: any, disposables?: extHostTypes.Disposable[]) {
return extHostAccountManagement.onDidChangeAccounts(listener, thisArgs, disposables);
@@ -197,12 +197,8 @@ export function createApiFactory(
extHostDataProvider.$onBatchComplete(provider.handle, batchInfo);
});
provider.registerOnResultSetAvailable((resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams) => {
extHostDataProvider.$onResultSetAvailable(provider.handle, resultSetInfo);
});
provider.registerOnResultSetUpdated((resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams) => {
extHostDataProvider.$onResultSetUpdated(provider.handle, resultSetInfo);
provider.registerOnResultSetComplete((resultSetInfo: sqlops.QueryExecuteResultSetCompleteNotificationParams) => {
extHostDataProvider.$onResultSetComplete(provider.handle, resultSetInfo);
});
provider.registerOnMessage((message: sqlops.QueryExecuteMessageParams) => {
@@ -314,6 +310,10 @@ export function createApiFactory(
return extHostDataProvider.$registerAgentServiceProvider(provider);
};
let registerDacFxServicesProvider = (provider: sqlops.DacFxServicesProvider): vscode.Disposable => {
return extHostDataProvider.$registerDacFxServiceProvider(provider);
};
// namespace: dataprotocol
const dataprotocol: typeof sqlops.dataprotocol = {
registerBackupProvider,
@@ -329,6 +329,7 @@ export function createApiFactory(
registerAdminServicesProvider,
registerAgentServicesProvider,
registerCapabilitiesServiceProvider,
registerDacFxServicesProvider,
onDidChangeLanguageFlavor(listener: (e: sqlops.DidChangeLanguageFlavorParams) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) {
return extHostDataProvider.onDidChangeLanguageFlavor(listener, thisArgs, disposables);
},
@@ -456,7 +457,8 @@ export function createApiFactory(
Orientation: sqlExtHostTypes.Orientation,
SqlThemeIcon: sqlExtHostTypes.SqlThemeIcon,
TreeComponentItem: sqlExtHostTypes.TreeComponentItem,
nb: nb
nb: nb,
AzureResource: sqlExtHostTypes.AzureResource
};
}
};

View File

@@ -27,7 +27,7 @@ import {
export abstract class ExtHostAccountManagementShape {
$autoOAuthCancelled(handle: number): Thenable<void> { throw ni(); }
$clear(handle: number, accountKey: sqlops.AccountKey): Thenable<void> { throw ni(); }
$getSecurityToken(account: sqlops.Account): Thenable<{}> { throw ni(); }
$getSecurityToken(account: sqlops.Account, resource?: sqlops.AzureResource): Thenable<{}> { throw ni(); }
$initialize(handle: number, restoredAccounts: sqlops.Account[]): Thenable<sqlops.Account[]> { throw ni(); }
$prompt(handle: number): Thenable<sqlops.Account> { throw ni(); }
$refresh(handle: number, account: sqlops.Account): Thenable<sqlops.Account> { throw ni(); }
@@ -193,11 +193,7 @@ export abstract class ExtHostDataProtocolShape {
/**
* Callback when a result set has been returned from query execution and can be displayed
*/
$onResultSetAvailable(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void { throw ni(); }
/**
* Callback when a result set has been returned from query execution and can be displayed
*/
$onResultSetUpdate(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void { throw ni(); }
$onResultSetComplete(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetCompleteNotificationParams): void { throw ni(); }
/**
* Callback when a message generated during query execution is issued
*/
@@ -413,6 +409,26 @@ export abstract class ExtHostDataProtocolShape {
* Get Agent Credentials list
*/
$getCredentials(handle: number, connectionUri: string): Thenable<sqlops.GetCredentialsResult> { throw ni(); }
/**
* DacFx export bacpac
*/
$exportBacpac(handle: number, databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> { throw ni(); }
/**
* DacFx import bacpac
*/
$importBacpac(handle: number, packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> { throw ni(); }
/**
* DacFx extract dacpac
*/
$extractDacpac(handle: number, databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> { throw ni(); }
/**
* DacFx deploy dacpac
*/
$deployDacpac(handle: number, packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> { throw ni(); }
}
/**
@@ -480,6 +496,7 @@ export interface MainThreadDataProtocolShape extends IDisposable {
$registerCapabilitiesServiceProvider(providerId: string, handle: number): TPromise<any>;
$registerAdminServicesProvider(providerId: string, handle: number): TPromise<any>;
$registerAgentServicesProvider(providerId: string, handle: number): TPromise<any>;
$registerDacFxServicesProvider(providerId: string, handle: number): TPromise<any>;
$unregisterProvider(handle: number): TPromise<any>;
$onConnectionComplete(handle: number, connectionInfoSummary: sqlops.ConnectionInfoSummary): void;
$onIntelliSenseCacheComplete(handle: number, connectionUri: string): void;
@@ -487,8 +504,7 @@ export interface MainThreadDataProtocolShape extends IDisposable {
$onQueryComplete(handle: number, result: sqlops.QueryExecuteCompleteNotificationResult): void;
$onBatchStart(handle: number, batchInfo: sqlops.QueryExecuteBatchNotificationParams): void;
$onBatchComplete(handle: number, batchInfo: sqlops.QueryExecuteBatchNotificationParams): void;
$onResultSetAvailable(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void;
$onResultSetUpdated(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetNotificationParams): void;
$onResultSetComplete(handle: number, resultSetInfo: sqlops.QueryExecuteResultSetCompleteNotificationParams): void;
$onQueryMessage(handle: number, message: sqlops.QueryExecuteMessageParams): void;
$onObjectExplorerSessionCreated(handle: number, message: sqlops.ObjectExplorerSession): void;
$onObjectExplorerSessionDisconnected(handle: number, message: sqlops.ObjectExplorerSession): void;

View File

@@ -136,7 +136,7 @@ suite('Firewall rule dialog controller tests', () => {
// Then: it should get security token from account management service and call create firewall rule in resource provider
deferredPromise.promise.then(() => {
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny()), TypeMoq.Times.once());
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockFirewallRuleDialog.verify(x => x.close(), TypeMoq.Times.once());
mockFirewallRuleDialog.verify(x => x.onServiceComplete(), TypeMoq.Times.once());
@@ -165,7 +165,7 @@ suite('Firewall rule dialog controller tests', () => {
// Then: it should get security token from account management service and an error dialog should have been opened
deferredPromise.promise.then(() => {
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny()), TypeMoq.Times.once());
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
done();
@@ -193,7 +193,7 @@ suite('Firewall rule dialog controller tests', () => {
// Then: it should get security token from account management service and an error dialog should have been opened
deferredPromise.promise.then(() => {
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny()), TypeMoq.Times.once());
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
done();
@@ -221,7 +221,7 @@ suite('Firewall rule dialog controller tests', () => {
// Then: it should get security token from account management service and an error dialog should have been opened
deferredPromise.promise.then(() => {
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny()), TypeMoq.Times.once());
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
done();
@@ -232,7 +232,7 @@ suite('Firewall rule dialog controller tests', () => {
function getMockAccountManagementService(resolveSecurityToken: boolean): TypeMoq.Mock<AccountManagementTestService> {
let accountManagementTestService = new AccountManagementTestService();
let mockAccountManagementService = TypeMoq.Mock.ofInstance(accountManagementTestService);
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny()))
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => resolveSecurityToken ? Promise.resolve({}) : Promise.reject(null).then());
return mockAccountManagementService;
}

View File

@@ -34,6 +34,7 @@ import * as assert from 'assert';
import * as TypeMoq from 'typemoq';
import { IConnectionProfileGroup, ConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup';
import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile';
import { AccountManagementTestService } from 'sqltest/stubs/accountManagementStubs';
suite('SQL ConnectionManagementService tests', () => {
@@ -46,6 +47,7 @@ suite('SQL ConnectionManagementService tests', () => {
let mssqlConnectionProvider: TypeMoq.Mock<ConnectionProviderStub>;
let workspaceConfigurationServiceMock: TypeMoq.Mock<WorkspaceConfigurationTestService>;
let resourceProviderStubMock: TypeMoq.Mock<ResourceProviderStub>;
let accountManagementService: TypeMoq.Mock<AccountManagementTestService>;
let none: void;
@@ -88,6 +90,7 @@ suite('SQL ConnectionManagementService tests', () => {
mssqlConnectionProvider = TypeMoq.Mock.ofType(ConnectionProviderStub);
let resourceProviderStub = new ResourceProviderStub();
resourceProviderStubMock = TypeMoq.Mock.ofInstance(resourceProviderStub);
accountManagementService = TypeMoq.Mock.ofType(AccountManagementTestService);
let root = new ConnectionProfileGroup(ConnectionProfileGroup.RootGroupName, undefined, ConnectionProfileGroup.RootGroupName, undefined, undefined);
root.connections = [ConnectionProfile.fromIConnectionProfile(capabilitiesService, connectionProfile)];
@@ -162,7 +165,8 @@ suite('SQL ConnectionManagementService tests', () => {
undefined,
resourceProviderStubMock.object,
undefined,
undefined
undefined,
accountManagementService.object
);
return connectionManagementService;
}
@@ -837,4 +841,44 @@ suite('SQL ConnectionManagementService tests', () => {
// Then undefined is returned
assert.equal(foundUri, undefined);
});
test('addSavedPassword fills in Azure access tokens for Azure accounts', async () => {
// Set up a connection profile that uses Azure
let azureConnectionProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, connectionProfile);
azureConnectionProfile.authenticationType = 'AzureMFA';
let username = 'testuser@microsoft.com';
azureConnectionProfile.userName = username;
let servername = 'test-database.database.windows.net';
azureConnectionProfile.serverName = servername;
// Set up the account management service to return a token for the given user
accountManagementService.setup(x => x.getAccountsForProvider(TypeMoq.It.isAny())).returns(providerId => Promise.resolve<sqlops.Account[]>([
{
key: {
accountId: username,
providerId: providerId
},
displayInfo: undefined,
isStale: false,
properties: undefined
}
]));
let testToken = 'testToken';
accountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
azurePublicCloud: {
token: testToken
}
}));
connectionStore.setup(x => x.addSavedPassword(TypeMoq.It.is(profile => profile.authenticationType === 'AzureMFA'))).returns(profile => Promise.resolve({
profile: profile,
savedCred: false
}));
// If I call addSavedPassword
let profileWithCredentials = await connectionManagementService.addSavedPassword(azureConnectionProfile);
// Then the returned profile has the account token set
assert.equal(profileWithCredentials.userName, username);
assert.equal(profileWithCredentials.options['azureAccountToken'], testToken);
});
});

View File

@@ -349,7 +349,7 @@ suite('SQL QueryEditor Tests', () => {
queryActionInstantiationService.setup(x => x.createInstance(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns((definition, editor, action, selectBox) => {
if (definition.ID === 'listDatabaseQueryActionItem') {
let item = new ListDatabasesActionItem(editor, action, queryConnectionService.object, undefined, undefined, undefined, configurationService.object);
let item = new ListDatabasesActionItem(editor, action, queryConnectionService.object, undefined, undefined, undefined,configurationService.object);
return item;
}
// Default

View File

@@ -50,7 +50,7 @@ export class AccountManagementTestService implements IAccountManagementService {
return undefined;
}
getSecurityToken(account: sqlops.Account): Thenable<{}> {
getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}> {
return undefined;
}
@@ -88,7 +88,7 @@ export class AccountProviderStub implements sqlops.AccountProvider {
return Promise.resolve();
}
getSecurityToken(account: sqlops.Account): Thenable<{}> {
getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}> {
return Promise.resolve({});
}

View File

@@ -15,7 +15,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/
import { IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier';
import { SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { MainThreadAccountManagement } from 'sql/workbench/api/node/mainThreadAccountManagement';
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
import { IAccountManagementService, AzureResource } from 'sql/services/accountManagement/interfaces';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
const IRPCProtocol = createDecorator<IRPCProtocol>('rpcProtocol');
@@ -366,7 +366,7 @@ suite('ExtHostAccountManagement', () => {
extHost.$getAllAccounts()
.then((accounts) => {
// If: I get security token it will not throw
return extHost.$getSecurityToken(mockAccount1);
return extHost.$getSecurityToken(mockAccount1, AzureResource.ResourceManagement);
}
).then(() => done(), (err) => done(new Error(err)));
});
@@ -417,7 +417,7 @@ suite('ExtHostAccountManagement', () => {
extHost.$getAllAccounts()
.then(accounts => {
return extHost.$getSecurityToken(mockAccount2);
return extHost.$getSecurityToken(mockAccount2, AzureResource.ResourceManagement);
})
.then((noError) => {
done(new Error('Expected getSecurityToken to throw'));
@@ -447,7 +447,7 @@ function getMockAccountManagementService(accounts: sqlops.Account[]): TypeMoq.Mo
mockAccountManagementService.setup(x => x.getAccountsForProvider(TypeMoq.It.isAny()))
.returns(() => Promise.resolve(accounts));
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isValue(accounts[0])))
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isValue(accounts[0]), TypeMoq.It.isAny()))
.returns(() => Promise.resolve({}));
mockAccountManagementService.setup(x => x.updateAccountListEvent)
.returns(() => () => { return undefined; } );

View File

@@ -830,7 +830,7 @@ declare namespace Slick {
* @param newData New databinding source using a regular JavaScript array..
* @param scrollToTop If true, the grid will reset the vertical scroll position to the top of the grid.
**/
public setData(newData: T[], scrollToTop?: boolean): void;
public setData(newData: T[], scrollToTop: boolean): void;
/**
* Sets a new source for databinding and removes all rendered rows. Note that this doesn't render the new rows - you can follow it with a call to render() to do that.

View File

@@ -137,6 +137,7 @@ import { IScriptingService, ScriptingService } from 'sql/services/scripting/scri
import { IAdminService, AdminService } from 'sql/parts/admin/common/adminService';
import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces';
import { JobManagementService } from 'sql/parts/jobManagement/common/jobManagementService';
import { IDacFxService, DacFxService } from 'sql/services/dacfx/dacFxService';
import { IBackupService, IBackupUiService } from 'sql/parts/disasterRecovery/backup/common/backupService';
import { BackupService, BackupUiService } from 'sql/parts/disasterRecovery/backup/common/backupServiceImp';
import { IRestoreDialogController, IRestoreService } from 'sql/parts/disasterRecovery/restore/common/restoreService';
@@ -557,6 +558,8 @@ export class Workbench extends Disposable implements IPartService {
serviceCollection.set(IServerGroupController, this.instantiationService.createInstance(ServerGroupController));
serviceCollection.set(ICredentialsService, this.instantiationService.createInstance(CredentialsService));
serviceCollection.set(IResourceProviderService, this.instantiationService.createInstance(ResourceProviderService));
let accountManagementService = this.instantiationService.createInstance(AccountManagementService, undefined);
serviceCollection.set(IAccountManagementService, accountManagementService);
let connectionManagementService = this.instantiationService.createInstance(ConnectionManagementService, undefined, undefined);
serviceCollection.set(IConnectionManagementService, connectionManagementService);
serviceCollection.set(ISerializationService, this.instantiationService.createInstance(SerializationService));
@@ -577,8 +580,6 @@ export class Workbench extends Disposable implements IPartService {
serviceCollection.set(IFileBrowserService, this.instantiationService.createInstance(FileBrowserService));
serviceCollection.set(IFileBrowserDialogController, this.instantiationService.createInstance(FileBrowserDialogController));
serviceCollection.set(IInsightsDialogService, this.instantiationService.createInstance(InsightsDialogService));
let accountManagementService = this.instantiationService.createInstance(AccountManagementService, undefined);
serviceCollection.set(IAccountManagementService, accountManagementService);
let notebookService = this.instantiationService.createInstance(NotebookService);
serviceCollection.set(INotebookService, notebookService);
serviceCollection.set(IAccountPickerService, this.instantiationService.createInstance(AccountPickerService));
@@ -586,6 +587,8 @@ export class Workbench extends Disposable implements IPartService {
// {{SQL CARBON EDIT}}
serviceCollection.set(ICommandLineProcessing, this.instantiationService.createInstance(CommandLineService));
// {{SQL CARBON EDIT}}
serviceCollection.set(IDacFxService, this.instantiationService.createInstance(DacFxService));
this._register(toDisposable(() => connectionManagementService.shutdown()));
this._register(toDisposable(() => accountManagementService.shutdown()));
this._register(toDisposable(() => notebookService.shutdown()));

3323
yarn.lock

File diff suppressed because it is too large Load Diff