Compare commits

...

28 Commits
1.4.0 ... 1.4.1

Author SHA1 Message Date
Karl Burtram
1263a27c1c Fix date in change log to 2019 (#3726) 2019-01-11 16:55:18 -08:00
Anthony Dresser
e1c084d365 fix html formatting in grid (#3722) 2019-01-11 16:24:50 -08:00
Karl Burtram
7465ec0bbd Add connection dialog icon dark theme and HC styles (#3721) 2019-01-11 13:38:43 -08:00
Chris LaFreniere
17ed57836f Fix focus issue when opening notebooks (#3711) 2019-01-11 11:37:49 -08:00
Chris LaFreniere
d0acb51fd7 Fix contentManager undefined when builtin manager used (#3710)
* Fix for contentManager undefined for builtin manager

* Clean up code some more
2019-01-11 10:36:36 -08:00
Anthony Dresser
71c1ed6c49 Add state for column sizing (#3683)
* add state for column sizing

* work properly with auto size columns
2019-01-11 10:25:57 -08:00
AlexFsmn
bfb68254a4 Added context menu for DBs in explorer view to backup & restore db. (#2277)
* Added context menu for DBs in explorer view to backup & restore db.
Fixed bug where progress bar didn't complete on backup/restore menuclick
#2084

* Fix merge conflicts
2019-01-11 10:00:16 -08:00
Anthony Dresser
18f7662209 Duplicate Result sets (#3620)
* remove debouncing and echoing to fix rendering bug

* fix access of internal member

* fix issue with using splice rather than slice

* fix compile issues
2019-01-10 13:44:14 -08:00
Karl Burtram
a0d84f383c Generate temp files as not dirty (#3698)
* Generate temp files as not dirty

* Remove whitespace
2019-01-10 12:51:41 -08:00
Karl Burtram
1f447ae681 Add Idera extension to recommendation list (#3709) 2019-01-10 11:54:39 -08:00
Kevin Cunnane
8bd6691331 Added v3 Notebook format support (#3697)
* Added v3 format support
2019-01-09 17:00:56 -08:00
Chris LaFreniere
42afcf9322 Integrate first SQL Notebooks Bits into Master (#3679)
* First crack tsql notebook (no output rendered yet)

* getting messages back

* intellisense working first cell, no connection errors

* sql notebook cell output functioning

* Latest SQL noteobook changes

* Undo change to launch.json

* Plumbing providers through

* Kernels shown from multiple providers, can switch between them. No mementos yet

* Ensure we have a feature flag for SQL notebooks, ensure existing functionality still works

* Fix tslint duplicate imports issue

* Addressing PR comments

* second round of PR feedback to cleanup notebook service manager code

* merge latest from master
2019-01-09 14:58:57 -08:00
David Shiflet
3d3694bb8d Add --command command line argument (#3690) 2019-01-09 17:36:01 -05:00
Anthony Dresser
589b913960 Readd Top Operations (#3628)
* workin on top operations

* added top operations, changed default sorter to handle number string better
2019-01-09 13:52:38 -08:00
kisantia
7ba4f42494 Moving onValidityChanged listener to showPage() so that it gets added to pages that are added to the wizard after the initial start up (#3691) 2019-01-09 13:19:24 -08:00
Chris LaFreniere
c96118d2b5 Fix activeCell nullref issue (#3689) 2019-01-09 11:45:05 -08:00
Karl Burtram
0285d8cd38 Update readme for January release (#3595)
* Update readme for December release

* Fix spelling

* Update release date to 12/13

* added release note items and fixed a small misspell

* Update release date to Dec 18

* Update release date
2019-01-09 10:12:14 -08:00
Matt Irvine
ee87604a4d Save grid selection/vertical scroll when switching tabs (#3682) 2019-01-08 15:51:57 -08:00
Kevin Cunnane
2235ebaf20 Fix #3680 Notebooks: outputs with string arrays rendered incorrectly (#3681)
* Refactor JSON and format files to model and fix tabs -> spaces issues

This is in prep for some work to reuse these code files inside the model,
so pushing as its own PR to keep the next piece of work clean.

* Fix #3680 Notebooks: outputs with string arrays rendered incorrectly
- Add support for processing v4 format files loaded from disk
- Prep support for v3 notebooks by adding placeholder code for that

* Fix failing tests and add specific one for this bug

* Remove references to v5
2019-01-08 15:24:16 -08:00
Anthony Dresser
954d0d954f Auto Column Sizing (#2778)
* add auto column sizing

* add break for performance

* update with new library
2019-01-08 13:05:53 -08:00
Kevin Cunnane
e31747d087 Refactor JSON and format files to model and fix tabs -> spaces issues (#3675)
This is in prep for some work to reuse these code files inside the model,
so pushing as its own PR to keep the next piece of work clean.
2019-01-07 14:02:12 -08:00
Anthony Dresser
fc581253a4 Fix gap with result streaming (#3629)
* handle updating item sizing when being updated

* change back scrolling delay

* remove unused code
2019-01-07 12:58:00 -08:00
Chris LaFreniere
47c4609f23 Ensure we call Dispose() on NotebookModel when notebook component is destroyed (#3667) 2019-01-04 12:00:42 -08:00
Chris LaFreniere
2d52bc2a49 Notebooks: Fix Selection/Focus when New Cells Added (#3649)
* Improvemnents to Active Cell

* Fix minor spacing issue

* fix editor focus order

* Fix for add cell above/below

* cleanup logic to have activeCell logic all reside in notebook model
2019-01-02 15:20:05 -08:00
kisantia
5367101330 Fix database not getting set correctly in DacFx wizard deploy scenario (#3641)
* fix db not getting set correctly for deploy scenario if coming from import page

* removed space so that comments go directly after //
2018-12-19 18:45:40 -05:00
Matt Irvine
db145b4999 Run TSLint in Azure Pipelines (#3639) 2018-12-19 12:17:23 -08:00
Matt Irvine
7f950ddb80 Update edit data for result set streaming changes (#3634) 2018-12-18 11:07:26 -08:00
Vincent Feng
50e2251e74 Feature/extensible azure resource explorer (#3504)
Extensible Azure Resource Explorer
2018-12-18 15:44:08 +08:00
160 changed files with 7918 additions and 5195 deletions

24
.vscode/launch.json vendored
View File

@@ -92,6 +92,30 @@
"webRoot": "${workspaceFolder}",
"timeout": 45000
},
{
"type": "chrome",
"request": "launch",
"name": "Launch azuredatastudio with new notebook command",
"windows": {
"runtimeExecutable": "${workspaceFolder}/scripts/sql.bat"
},
"osx": {
"runtimeExecutable": "${workspaceFolder}/scripts/sql.sh"
},
"linux": {
"runtimeExecutable": "${workspaceFolder}/scripts/sql.sh"
},
"urlFilter": "*index.html*",
"runtimeArgs": [
"--inspect=5875",
"--command=notebook.command.new"
],
"skipFiles": [
"**/winjs*.js"
],
"webRoot": "${workspaceFolder}",
"timeout": 45000
},
{
"type": "node",
"request": "launch",

View File

@@ -1,5 +1,27 @@
# Change Log
## Version 1.3.8
* Release date: January 9, 2019
* Release status: General Availability
## What's new in this version
* #13 Feature Request: Azure Active Directory Authentication
* #1040 Stream initial query results as they become available
* #3298 Сan't add an azure account.
* #2387 Support Per-User Installer
* SQL Server Import updates for DACPAC\BACPAC
* SQL Server Profiler UI and UX improvements
* Updates to [SQL Server 2019 extension](https://docs.microsoft.com/sql/azure-data-studio/sql-server-2019-extension?view=sql-server-ver15)
* **sp_executesql to SQL** and **New Database** extensions
## Contributions and "thank you"
We would like to thank all our users who raised issues, and in particular the following users who helped contribute fixes:
* Tarig0 for `Add Routine_Type to CreateStoredProc fixes #3257 (#3286)`
* oltruong for `typo fix #3025'`
* Thomas-S-B for `Removed unnecessary IErrorDetectionStrategy #749`
* Thomas-S-B for `Simplified code #750`
## Version 1.2.4
* Release date: November 6, 2018
* Release status: General Availability

View File

@@ -9,12 +9,13 @@ Azure Data Studio is a data management tool that enables you to work with SQL Se
Platform | Link
-- | --
Windows Setup Installer | https://go.microsoft.com/fwlink/?linkid=2038320
Windows ZIP | https://go.microsoft.com/fwlink/?linkid=2038323
macOS ZIP | https://go.microsoft.com/fwlink/?linkid=2038327
Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=2038332
Linux RPM | https://go.microsoft.com/fwlink/?linkid=2038401
Linux DEB | https://go.microsoft.com/fwlink/?linkid=2038405
Windows User Installer | https://go.microsoft.com/fwlink/?linkid=2049972
Windows System Installer | https://go.microsoft.com/fwlink/?linkid=2049975
Windows ZIP | https://go.microsoft.com/fwlink/?linkid=2050146
macOS ZIP | https://go.microsoft.com/fwlink/?linkid=2049981
Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=2049986
Linux RPM | https://go.microsoft.com/fwlink/?linkid=2049989
Linux DEB | https://go.microsoft.com/fwlink/?linkid=2050157
Go to our [download page](https://aka.ms/azuredatastudio) for more specific instructions.
@@ -62,6 +63,10 @@ The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.micro
## Contributions and "Thank You"
We would like to thank all our users who raised issues, and in particular the following users who helped contribute fixes:
* Tarig0 for `Add Routine_Type to CreateStoredProc fixes #3257 (#3286)`
* oltruong for `typo fix #3025'`
* Thomas-S-B for `Removed unnecessary IErrorDetectionStrategy #749`
* Thomas-S-B for `Simplified code #750`
* rdaniels6813 for `Add query plan theme support #3031`
* Ruturaj123 for `Fixed some typos and grammatical errors #3027`
* PromoFaux for `Use emoji shortcodes in CONTRIBUTING.md instead of <20> #3009`

View File

@@ -34,5 +34,9 @@ steps:
- task: PublishTestResults@2
inputs:
testResultsFiles: '**/test-results.xml'
condition: succeededOrFailed()
testResultsFiles: '**/test-results.xml'
condition: succeededOrFailed()
- script: |
yarn run tslint
displayName: 'Run TSLint'

View File

@@ -23,4 +23,8 @@ steps:
- task: PublishTestResults@2
inputs:
testResultsFiles: 'test-results.xml'
condition: succeededOrFailed()
condition: succeededOrFailed()
- script: |
yarn run tslint
displayName: 'Run TSLint'

View File

@@ -17,12 +17,12 @@
"configuration": [
{
"type": "object",
"title": "%azure.config.title%",
"title": "%azure.resource.config.title%",
"properties": {
"azureResource.resourceFilter": {
"azure.resource.config.filter": {
"type": "array",
"default": null,
"description": "%azure.resourceFilter.description%"
"description": "%azure.resource.config.filter.description%"
}
}
},
@@ -61,39 +61,47 @@
"category": "Azure Accounts"
},
{
"command": "azureresource.refreshall",
"title": "%azureresource.refreshall%",
"command": "azure.resource.refreshall",
"title": "%azure.resource.refreshall.title%",
"icon": {
"dark": "resources/dark/refresh_inverse.svg",
"light": "resources/light/refresh.svg"
}
},
{
"command": "azureresource.refresh",
"title": "%azureresource.refresh%",
"command": "azure.resource.refresh",
"title": "%azure.resource.refresh.title%",
"icon": {
"dark": "resources/dark/refresh_inverse.svg",
"light": "resources/light/refresh.svg"
}
},
{
"command": "azureresource.signin",
"title": "%azureresource.signin%"
"command": "azure.resource.signin",
"title": "%azure.resource.signin.title%"
},
{
"command": "azureresource.connectsqldb",
"title": "%azureresource.connectsqldb%",
"command": "azure.resource.selectsubscriptions",
"title": "%azure.resource.selectsubscriptions.title%",
"icon": {
"dark": "resources/dark/filter_inverse.svg",
"light": "resources/light/filter.svg"
}
},
{
"command": "azure.resource.connectsqlserver",
"title": "%azure.resource.connectsqlserver.title%",
"icon": {
"dark": "resources/dark/connect_to_inverse.svg",
"light": "resources/light/connect_to.svg"
}
},
{
"command": "azureresource.selectsubscriptions",
"title": "%azureresource.selectsubscriptions%",
"command": "azure.resource.connectsqldb",
"title": "%azure.resource.connectsqldb.title%",
"icon": {
"dark": "resources/dark/filter_inverse.svg",
"light": "resources/light/filter.svg"
"dark": "resources/dark/connect_to_inverse.svg",
"light": "resources/light/connect_to.svg"
}
}
],
@@ -110,46 +118,47 @@
"azureResource": [
{
"id": "azureResourceExplorer",
"name": "%azure.resourceExplorer.title%"
"name": "%azure.resource.explorer.title%"
}
]
},
"menus": {
"view/title": [
{
"command": "azureresource.refreshall",
"command": "azure.resource.refreshall",
"when": "view == azureResourceExplorer",
"group": "navigation@1"
}
],
"view/item/context": [
{
"command": "azureresource.connectsqldb",
"when": "viewItem =~ /^azureResource\\.itemType\\.database(?:Server){0,1}$/",
"group": "1azureresource@1"
},
{
"command": "azureresource.connectsqldb",
"when": "viewItem =~ /^azureResource\\.itemType\\.database(?:Server){0,1}$/",
"command": "azure.resource.selectsubscriptions",
"when": "viewItem == azure.resource.itemType.account",
"group": "inline"
},
{
"command": "azureresource.selectsubscriptions",
"when": "viewItem == azureResource.itemType.account",
"command": "azure.resource.refresh",
"when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
"group": "inline"
},
{
"command": "azureresource.refresh",
"when": "viewItem =~ /^azureResource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
"command": "azure.resource.connectsqlserver",
"when": "viewItem == azure.resource.itemType.databaseServer",
"group": "inline"
},
{
"command": "azure.resource.connectsqldb",
"when": "viewItem == azure.resource.itemType.database",
"group": "inline"
}
]
}
},
"hasAzureResourceProviders": true
},
"dependencies": {
"request": "2.88.0",
"azure-arm-resource": "^7.0.0",
"azure-arm-sql": "^5.0.1",
"request": "2.88.0",
"vscode-nls": "^4.0.0"
},
"devDependencies": {
@@ -157,6 +166,7 @@
"@types/node": "^8.0.24",
"mocha": "^5.2.0",
"should": "^13.2.1",
"vscode": "^1.1.26",
"typemoq": "^2.1.0"
}
}
}

View File

@@ -1,16 +1,20 @@
{
"azure.displayName": "Azure (Core)",
"azure.description": "Browse and work with Azure resources",
"azure.config.title": "Azure Resource Configuration",
"azure.resourceFilter.description": "The resource filter, each element is an account id, a subscription id and name separated by a slash",
"azureresource.refreshall": "Refresh All",
"azureresource.refresh": "Refresh",
"azureresource.signin": "Sign In",
"azureresource.connectsqldb": "Connect",
"azureresource.selectsubscriptions": "Select Subscriptions",
"azure.title": "Azure",
"azure.resourceExplorer.title": "Resource Explorer",
"azure.resource.config.title": "Azure Resource Configuration",
"azure.resource.config.filter.description": "The resource filter, each element is an account id, a subscription id and name separated by a slash",
"azure.resource.explorer.title": "Resource Explorer",
"azure.resource.refreshall.title": "Refresh All",
"azure.resource.refresh.title": "Refresh",
"azure.resource.signin.title": "Sign In",
"azure.resource.selectsubscriptions.title": "Select Subscriptions",
"azure.resource.connectsqlserver.title": "Connect",
"azure.resource.connectsqldb.title": "Connect",
"accounts.clearTokenCache": "Clear Azure Account Token Cache",
"config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled",
"config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled",
"config.enableChinaCloudDescription": "Should Azure China integration be enabled",

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TreeDataProvider, TreeItem } from 'vscode';
import { DataProvider, Account } from 'sqlops';
export namespace azureResource {
export interface IAzureResourceProvider extends DataProvider {
getTreeDataProvider(): IAzureResourceTreeDataProvider;
}
export interface IAzureResourceTreeDataProvider extends TreeDataProvider<IAzureResourceNode> {
}
export interface IAzureResourceNode {
readonly account: Account;
readonly subscription: AzureResourceSubscription;
readonly tenantId: string;
readonly treeItem: TreeItem;
}
export interface AzureResourceSubscription {
id: string;
name: string;
}
}

View File

@@ -6,35 +6,48 @@
'use strict';
import { window, QuickPickItem } from 'vscode';
import * as sqlops from 'sqlops';
import { generateGuid } from './utils';
import { ApiWrapper } from '../apiWrapper';
import { TreeNode } from '../treeNodes';
import { AzureResource } from 'sqlops';
import { TokenCredentials } from 'ms-rest';
import { AppContext } from '../appContext';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { azureResource } from './azure-resource';
import { TreeNode } from './treeNode';
import { AzureResourceCredentialError } from './errors';
import { AzureResourceTreeProvider } from './tree/treeProvider';
import { AzureResourceDatabaseServerTreeNode } from './tree/databaseServerTreeNode';
import { AzureResourceDatabaseTreeNode } from './tree/databaseTreeNode';
import { AzureResourceAccountTreeNode } from './tree/accountTreeNode';
import { AzureResourceServicePool } from './servicePool';
import { AzureResourceSubscription } from './models';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from './constants';
export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: AzureResourceTreeProvider): void {
apiWrapper.registerCommand('azureresource.selectsubscriptions', async (node?: TreeNode) => {
export function registerAzureResourceCommands(appContext: AppContext, tree: AzureResourceTreeProvider): void {
appContext.apiWrapper.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => {
if (!(node instanceof AzureResourceAccountTreeNode)) {
return;
}
const subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
const subscriptionFilterService = appContext.getService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
const accountNode = node as AzureResourceAccountTreeNode;
const servicePool = AzureResourceServicePool.getInstance();
const subscriptions = (await accountNode.getCachedSubscriptions()) || <azureResource.AzureResourceSubscription[]>[];
if (subscriptions.length === 0) {
try {
const tokens = await this.servicePool.apiWrapper.getSecurityToken(this.account, AzureResource.ResourceManagement);
let subscriptions = await accountNode.getCachedSubscriptions();
if (!subscriptions || subscriptions.length === 0) {
const credentials = await servicePool.credentialService.getCredentials(accountNode.account, sqlops.AzureResource.ResourceManagement);
subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials);
for (const tenant of this.account.properties.tenants) {
const token = tokens[tenant.id].token;
const tokenType = tokens[tenant.id].tokenType;
subscriptions.push(...await subscriptionService.getSubscriptions(accountNode.account, new TokenCredentials(token, tokenType)));
}
} catch (error) {
throw new AzureResourceCredentialError(localize('azure.resource.selectsubscriptions.credentialError', 'Failed to get credential for account {0}. Please refresh the account.', this.account.key.accountId), error);
}
}
const selectedSubscriptions = (await servicePool.subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || <AzureResourceSubscription[]>[];
let selectedSubscriptions = (await subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || <azureResource.AzureResourceSubscription[]>[];
const selectedSubscriptionIds: string[] = [];
if (selectedSubscriptions.length > 0) {
selectedSubscriptionIds.push(...selectedSubscriptions.map((subscription) => subscription.id));
@@ -43,11 +56,11 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur
selectedSubscriptionIds.push(...subscriptions.map((subscription) => subscription.id));
}
interface SubscriptionQuickPickItem extends QuickPickItem {
subscription: AzureResourceSubscription;
interface AzureResourceSubscriptionQuickPickItem extends QuickPickItem {
subscription: azureResource.AzureResourceSubscription;
}
const subscriptionItems: SubscriptionQuickPickItem[] = subscriptions.map((subscription) => {
const subscriptionQuickPickItems: AzureResourceSubscriptionQuickPickItem[] = subscriptions.map((subscription) => {
return {
label: subscription.name,
picked: selectedSubscriptionIds.indexOf(subscription.id) !== -1,
@@ -55,66 +68,22 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur
};
});
const pickedSubscriptionItems = (await window.showQuickPick(subscriptionItems, { canPickMany: true }));
if (pickedSubscriptionItems && pickedSubscriptionItems.length > 0) {
const selectedSubscriptionQuickPickItems = (await window.showQuickPick(subscriptionQuickPickItems, { canPickMany: true }));
if (selectedSubscriptionQuickPickItems && selectedSubscriptionQuickPickItems.length > 0) {
tree.refresh(node, false);
const pickedSubscriptions = pickedSubscriptionItems.map((subscriptionItem) => subscriptionItem.subscription);
await servicePool.subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, pickedSubscriptions);
selectedSubscriptions = selectedSubscriptionQuickPickItems.map((subscriptionItem) => subscriptionItem.subscription);
await subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, selectedSubscriptions);
}
});
apiWrapper.registerCommand('azureresource.refreshall', () => tree.notifyNodeChanged(undefined));
appContext.apiWrapper.registerCommand('azure.resource.refreshall', () => tree.notifyNodeChanged(undefined));
apiWrapper.registerCommand('azureresource.refresh', async (node?: TreeNode) => {
appContext.apiWrapper.registerCommand('azure.resource.refresh', async (node?: TreeNode) => {
tree.refresh(node, true);
});
apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => {
let connectionProfile: sqlops.IConnectionProfile = {
id: generateGuid(),
connectionName: undefined,
serverName: undefined,
databaseName: undefined,
userName: undefined,
password: '',
authenticationType: undefined,
savePassword: true,
groupFullName: '',
groupId: '',
providerName: undefined,
saveProfile: true,
options: {
}
};
if (node instanceof AzureResourceDatabaseServerTreeNode) {
let databaseServer = node.databaseServer;
connectionProfile.connectionName = `connection to '${databaseServer.defaultDatabaseName}' on '${databaseServer.fullName}'`;
connectionProfile.serverName = databaseServer.fullName;
connectionProfile.databaseName = databaseServer.defaultDatabaseName;
connectionProfile.userName = databaseServer.loginName;
connectionProfile.authenticationType = 'SqlLogin';
connectionProfile.providerName = 'MSSQL';
}
if (node instanceof AzureResourceDatabaseTreeNode) {
let database = node.database;
connectionProfile.connectionName = `connection to '${database.name}' on '${database.serverFullName}'`;
connectionProfile.serverName = database.serverFullName;
connectionProfile.databaseName = database.name;
connectionProfile.userName = database.loginName;
connectionProfile.authenticationType = 'SqlLogin';
connectionProfile.providerName = 'MSSQL';
}
const conn = await apiWrapper.openConnectionDialog(undefined, connectionProfile, { saveConnection: true, showDashboard: true });
if (conn) {
apiWrapper.executeCommand('workbench.view.connections');
}
});
apiWrapper.registerCommand('azureresource.signin', async (node?: TreeNode) => {
apiWrapper.executeCommand('sql.action.accounts.manageLinkedAccount');
appContext.apiWrapper.registerCommand('azure.resource.signin', async (node?: TreeNode) => {
appContext.apiWrapper.executeCommand('sql.action.accounts.manageLinkedAccount');
});
}

View File

@@ -6,11 +6,19 @@
'use strict';
export enum AzureResourceItemType {
account = 'azureResource.itemType.account',
subscription = 'azureResource.itemType.subscription',
databaseContainer = 'azureResource.itemType.databaseContainer',
database = 'azureResource.itemType.database',
databaseServerContainer = 'azureResource.itemType.databaseServerContainer',
databaseServer = 'azureResource.itemType.databaseServer',
message = 'azureResource.itemType.message'
account = 'azure.resource.itemType.account',
subscription = 'azure.resource.itemType.subscription',
databaseContainer = 'azure.resource.itemType.databaseContainer',
database = 'azure.resource.itemType.database',
databaseServerContainer = 'azure.resource.itemType.databaseServerContainer',
databaseServer = 'azure.resource.itemType.databaseServer',
message = 'azure.resource.itemType.message'
}
export enum AzureResourceServiceNames {
cacheService = 'AzureResourceCacheService',
accountService = 'AzureResourceAccountService',
subscriptionService = 'AzureResourceSubscriptionService',
subscriptionFilterService = 'AzureResourceSubscriptionFilterService',
tenantService = 'AzureResourceTenantService'
}

View File

@@ -8,7 +8,7 @@
export class AzureResourceCredentialError extends Error {
constructor(
message: string,
public innerError: Error
public readonly innerError: Error
) {
super(message);
}

View File

@@ -6,49 +6,40 @@
'use strict';
import { ServiceClientCredentials } from 'ms-rest';
import * as sqlops from 'sqlops';
import { Account, DidChangeAccountsParams } from 'sqlops';
import { Event } from 'vscode';
import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models';
import { azureResource } from './azure-resource';
export interface IAzureResourceAccountService {
getAccounts(): Promise<sqlops.Account[]>;
getAccounts(): Promise<Account[]>;
readonly onDidChangeAccounts: Event<sqlops.DidChangeAccountsParams>;
}
export interface IAzureResourceCredentialService {
getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise<ServiceClientCredentials[]>;
readonly onDidChangeAccounts: Event<DidChangeAccountsParams>;
}
export interface IAzureResourceSubscriptionService {
getSubscriptions(account: sqlops.Account, credentials: ServiceClientCredentials[]): Promise<AzureResourceSubscription[]>;
getSubscriptions(account: Account, credential: ServiceClientCredentials): Promise<azureResource.AzureResourceSubscription[]>;
}
export interface IAzureResourceSubscriptionFilterService {
getSelectedSubscriptions(account: sqlops.Account): Promise<AzureResourceSubscription[]>;
getSelectedSubscriptions(account: Account): Promise<azureResource.AzureResourceSubscription[]>;
saveSelectedSubscriptions(account: sqlops.Account, selectedSubscriptions: AzureResourceSubscription[]): Promise<void>;
}
export interface IAzureResourceDatabaseServerService {
getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise<AzureResourceDatabaseServer[]>;
}
export interface IAzureResourceDatabaseService {
getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise<AzureResourceDatabase[]>;
saveSelectedSubscriptions(account: Account, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise<void>;
}
export interface IAzureResourceCacheService {
generateKey(id: string): string;
get<T>(key: string): T | undefined;
update<T>(key: string, value: T): void;
}
export interface IAzureResourceContextService {
getAbsolutePath(relativePath: string): string;
executeCommand(commandId: string, ...args: any[]): void;
showErrorMessage(errorMessage: string): void;
export interface IAzureResourceTenantService {
getTenantId(subscription: azureResource.AzureResourceSubscription): Promise<string>;
}
export interface IAzureResourceNodeWithProviderId {
resourceProviderId: string;
resourceNode: azureResource.IAzureResourceNode;
}

View File

@@ -7,9 +7,9 @@
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import { AzureResourceItemType } from '../constants';
import { TreeNode } from './treeNode';
import { AzureResourceItemType } from './constants';
export class AzureResourceMessageTreeNode extends TreeNode {
public constructor(

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IConnectionProfile } from 'sqlops';
import { AppContext } from '../../../appContext';
import { TreeNode } from '../../treeNode';
import { generateGuid } from '../../utils';
import { AzureResourceItemType } from '../../constants';
import { IAzureResourceDatabaseNode } from './interfaces';
import { AzureResourceResourceTreeNode } from '../../resourceTreeNode';
export function registerAzureResourceDatabaseCommands(appContext: AppContext): void {
appContext.apiWrapper.registerCommand('azure.resource.connectsqldb', async (node?: TreeNode) => {
if (!node)
{
return;
}
const treeItem = await node.getTreeItem();
if (treeItem.contextValue !== AzureResourceItemType.database) {
return;
}
const resourceNode = (node as AzureResourceResourceTreeNode).resourceNodeWithProviderId.resourceNode;
const database = (resourceNode as IAzureResourceDatabaseNode).database;
let connectionProfile: IConnectionProfile = {
id: generateGuid(),
connectionName: undefined,
serverName: database.serverFullName,
databaseName: database.name,
userName: database.loginName,
password: '',
authenticationType: 'SqlLogin',
savePassword: true,
groupFullName: '',
groupId: '',
providerName: 'MSSQL',
saveProfile: true,
options: {}
};
const conn = await appContext.apiWrapper.openConnectionDialog(undefined, connectionProfile, { saveConnection: true, showDashboard: true });
if (conn) {
appContext.apiWrapper.executeCommand('workbench.view.connections');
}
});
}

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ExtensionContext } from 'vscode';
import { ApiWrapper } from '../../../apiWrapper';
import { azureResource } from '../../azure-resource';
import { IAzureResourceDatabaseService } from './interfaces';
import { AzureResourceDatabaseTreeDataProvider } from './databaseTreeDataProvider';
export class AzureResourceDatabaseProvider implements azureResource.IAzureResourceProvider {
public constructor(
databaseService: IAzureResourceDatabaseService,
apiWrapper: ApiWrapper,
extensionContext: ExtensionContext
) {
this._databaseService = databaseService;
this._apiWrapper = apiWrapper;
this._extensionContext = extensionContext;
}
public getTreeDataProvider(): azureResource.IAzureResourceTreeDataProvider {
return new AzureResourceDatabaseTreeDataProvider(this._databaseService, this._apiWrapper, this._extensionContext);
}
public get providerId(): string {
return 'azure.resource.providers.database';
}
private _databaseService: IAzureResourceDatabaseService = undefined;
private _apiWrapper: ApiWrapper = undefined;
private _extensionContext: ExtensionContext = undefined;
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* 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 { ServiceClientCredentials } from 'ms-rest';
import { SqlManagementClient } from 'azure-arm-sql';
import { azureResource } from '../../azure-resource';
import { IAzureResourceDatabaseService } from './interfaces';
import { AzureResourceDatabase } from './models';
export class AzureResourceDatabaseService implements IAzureResourceDatabaseService {
public async getDatabases(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise<AzureResourceDatabase[]> {
const databases: AzureResourceDatabase[] = [];
const sqlManagementClient = new SqlManagementClient(credential, subscription.id);
const svrs = await sqlManagementClient.servers.list();
for (const svr of svrs) {
// Extract resource group name from svr.id
const svrIdRegExp = new RegExp(`\/subscriptions\/${subscription.id}\/resourceGroups\/(.+)\/providers\/Microsoft\.Sql\/servers\/${svr.name}`);
if (!svrIdRegExp.test(svr.id)) {
continue;
}
const founds = svrIdRegExp.exec(svr.id);
const resouceGroup = founds[1];
const dbs = await sqlManagementClient.databases.listByServer(resouceGroup, svr.name);
dbs.forEach((db) => databases.push({
name: db.name,
serverName: svr.name,
serverFullName: svr.fullyQualifiedDomainName,
loginName: svr.administratorLogin
}));
}
return databases;
}
}

View File

@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* 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 { AzureResource } from 'sqlops';
import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode';
import { TokenCredentials } from 'ms-rest';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { azureResource } from '../../azure-resource';
import { IAzureResourceDatabaseService, IAzureResourceDatabaseNode } from './interfaces';
import { AzureResourceDatabase } from './models';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { ApiWrapper } from '../../../apiWrapper';
export class AzureResourceDatabaseTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider {
public constructor(
databaseService: IAzureResourceDatabaseService,
apiWrapper: ApiWrapper,
extensionContext: ExtensionContext
) {
this._databaseService = databaseService;
this._apiWrapper = apiWrapper;
this._extensionContext = extensionContext;
}
public getTreeItem(element: azureResource.IAzureResourceNode): TreeItem | Thenable<TreeItem> {
return element.treeItem;
}
public async getChildren(element?: azureResource.IAzureResourceNode): Promise<azureResource.IAzureResourceNode[]> {
if (!element) {
return [this.createContainerNode()];
}
const tokens = await this._apiWrapper.getSecurityToken(element.account, AzureResource.ResourceManagement);
const credential = new TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType);
const databases: AzureResourceDatabase[] = (await this._databaseService.getDatabases(element.subscription, credential)) || <AzureResourceDatabase[]>[];
return databases.map((database) => <IAzureResourceDatabaseNode>{
account: element.account,
subscription: element.subscription,
tenantId: element.tenantId,
database: database,
treeItem: {
id: `databaseServer_${database.serverFullName}.database_${database.name}`,
label: `${database.name} (${database.serverName})`,
iconPath: {
dark: this._extensionContext.asAbsolutePath('resources/dark/sql_database_inverse.svg'),
light: this._extensionContext.asAbsolutePath('resources/light/sql_database.svg')
},
collapsibleState: TreeItemCollapsibleState.None,
contextValue: AzureResourceItemType.database
}
});
}
private createContainerNode(): azureResource.IAzureResourceNode {
return {
account: undefined,
subscription: undefined,
tenantId: undefined,
treeItem: {
id: AzureResourceDatabaseTreeDataProvider.containerId,
label: AzureResourceDatabaseTreeDataProvider.containerLabel,
iconPath: {
dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
},
collapsibleState: TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseContainer
}
};
}
private _databaseService: IAzureResourceDatabaseService = undefined;
private _apiWrapper: ApiWrapper = undefined;
private _extensionContext: ExtensionContext = undefined;
private static readonly containerId = 'azure.resource.providers.database.treeDataProvider.databaseContainer';
private static readonly containerLabel = localize('azure.resource.providers.database.treeDataProvider.databaseContainerLabel', 'SQL Databases');
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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 { ServiceClientCredentials } from 'ms-rest';
import { azureResource } from '../../azure-resource';
import { AzureResourceDatabase } from './models';
export interface IAzureResourceDatabaseService {
getDatabases(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise<AzureResourceDatabase[]>;
}
export interface IAzureResourceDatabaseNode extends azureResource.IAzureResourceNode {
readonly database: AzureResourceDatabase;
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export interface AzureResourceDatabase {
name: string;
serverName: string;
serverFullName: string;
loginName: string;
}

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IConnectionProfile } from 'sqlops';
import { AppContext } from '../../../appContext';
import { TreeNode } from '../../treeNode';
import { generateGuid } from '../../utils';
import { AzureResourceItemType } from '../../constants';
import { IAzureResourceDatabaseServerNode } from './interfaces';
import { AzureResourceResourceTreeNode } from '../../resourceTreeNode';
export function registerAzureResourceDatabaseServerCommands(appContext: AppContext): void {
appContext.apiWrapper.registerCommand('azure.resource.connectsqlserver', async (node?: TreeNode) => {
if (!node)
{
return;
}
const treeItem = await node.getTreeItem();
if (treeItem.contextValue !== AzureResourceItemType.databaseServer) {
return;
}
const resourceNode = (node as AzureResourceResourceTreeNode).resourceNodeWithProviderId.resourceNode;
const databaseServer = (resourceNode as IAzureResourceDatabaseServerNode).databaseServer;
let connectionProfile: IConnectionProfile = {
id: generateGuid(),
connectionName: undefined,
serverName: databaseServer.fullName,
databaseName: databaseServer.defaultDatabaseName,
userName: databaseServer.loginName,
password: '',
authenticationType: 'SqlLogin',
savePassword: true,
groupFullName: '',
groupId: '',
providerName: 'MSSQL',
saveProfile: true,
options: {}
};
const conn = await appContext.apiWrapper.openConnectionDialog(undefined, connectionProfile, { saveConnection: true, showDashboard: true });
if (conn) {
appContext.apiWrapper.executeCommand('workbench.view.connections');
}
});
}

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ExtensionContext } from 'vscode';
import { ApiWrapper } from '../../../apiWrapper';
import { azureResource } from '../../azure-resource';
import { IAzureResourceDatabaseServerService } from './interfaces';
import { AzureResourceDatabaseServerTreeDataProvider } from './databaseServerTreeDataProvider';
export class AzureResourceDatabaseServerProvider implements azureResource.IAzureResourceProvider {
public constructor(
databaseServerService: IAzureResourceDatabaseServerService,
apiWrapper: ApiWrapper,
extensionContext: ExtensionContext
) {
this._databaseServerService = databaseServerService;
this._apiWrapper = apiWrapper;
this._extensionContext = extensionContext;
}
public getTreeDataProvider(): azureResource.IAzureResourceTreeDataProvider {
return new AzureResourceDatabaseServerTreeDataProvider(this._databaseServerService, this._apiWrapper, this._extensionContext);
}
public get providerId(): string {
return 'azure.resource.providers.databaseServer';
}
private _databaseServerService: IAzureResourceDatabaseServerService = undefined;
private _apiWrapper: ApiWrapper = undefined;
private _extensionContext: ExtensionContext = undefined;
}

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* 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 { ServiceClientCredentials } from 'ms-rest';
import { SqlManagementClient } from 'azure-arm-sql';
import { azureResource } from '../../azure-resource';
import { IAzureResourceDatabaseServerService } from './interfaces';
import { AzureResourceDatabaseServer } from './models';
export class AzureResourceDatabaseServerService implements IAzureResourceDatabaseServerService {
public async getDatabaseServers(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise<AzureResourceDatabaseServer[]> {
const databaseServers: AzureResourceDatabaseServer[] = [];
const sqlManagementClient = new SqlManagementClient(credential, subscription.id);
const svrs = await sqlManagementClient.servers.list();
svrs.forEach((svr) => databaseServers.push({
name: svr.name,
fullName: svr.fullyQualifiedDomainName,
loginName: svr.administratorLogin,
defaultDatabaseName: 'master'
}));
return databaseServers;
}
}

View File

@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* 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 { AzureResource } from 'sqlops';
import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode';
import { TokenCredentials } from 'ms-rest';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { azureResource } from '../../azure-resource';
import { IAzureResourceDatabaseServerService, IAzureResourceDatabaseServerNode } from './interfaces';
import { AzureResourceDatabaseServer } from './models';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { ApiWrapper } from '../../../apiWrapper';
export class AzureResourceDatabaseServerTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider {
public constructor(
databaseServerService: IAzureResourceDatabaseServerService,
apiWrapper: ApiWrapper,
extensionContext: ExtensionContext
) {
this._databaseServerService = databaseServerService;
this._apiWrapper = apiWrapper;
this._extensionContext = extensionContext;
}
public getTreeItem(element: azureResource.IAzureResourceNode): TreeItem | Thenable<TreeItem> {
return element.treeItem;
}
public async getChildren(element?: azureResource.IAzureResourceNode): Promise<azureResource.IAzureResourceNode[]> {
if (!element) {
return [this.createContainerNode()];
}
const tokens = await this._apiWrapper.getSecurityToken(element.account, AzureResource.ResourceManagement);
const credential = new TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType);
const databaseServers: AzureResourceDatabaseServer[] = (await this._databaseServerService.getDatabaseServers(element.subscription, credential)) || <AzureResourceDatabaseServer[]>[];
return databaseServers.map((databaseServer) => <IAzureResourceDatabaseServerNode>{
account: element.account,
subscription: element.subscription,
tenantId: element.tenantId,
databaseServer: databaseServer,
treeItem: {
id: `databaseServer_${databaseServer.name}`,
label: databaseServer.name,
iconPath: {
dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'),
light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg')
},
collapsibleState: TreeItemCollapsibleState.None,
contextValue: AzureResourceItemType.databaseServer
}
});
}
private createContainerNode(): azureResource.IAzureResourceNode {
return {
account: undefined,
subscription: undefined,
tenantId: undefined,
treeItem: {
id: AzureResourceDatabaseServerTreeDataProvider.containerId,
label: AzureResourceDatabaseServerTreeDataProvider.containerLabel,
iconPath: {
dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
},
collapsibleState: TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServerContainer
}
};
}
private _databaseServerService: IAzureResourceDatabaseServerService = undefined;
private _apiWrapper: ApiWrapper = undefined;
private _extensionContext: ExtensionContext = undefined;
private static readonly containerId = 'azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainer';
private static readonly containerLabel = localize('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainerLabel', 'SQL Servers');
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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 { ServiceClientCredentials } from 'ms-rest';
import { azureResource } from '../../azure-resource';
import { AzureResourceDatabaseServer } from './models';
export interface IAzureResourceDatabaseServerService {
getDatabaseServers(subscription: azureResource.AzureResourceSubscription, credentials: ServiceClientCredentials): Promise<AzureResourceDatabaseServer[]>;
}
export interface IAzureResourceDatabaseServerNode extends azureResource.IAzureResourceNode {
readonly databaseServer: AzureResourceDatabaseServer;
}

View File

@@ -5,21 +5,9 @@
'use strict';
export interface AzureResourceSubscription {
id: string;
name: string;
}
export interface AzureResourceDatabaseServer {
name: string;
fullName: string;
loginName: string;
defaultDatabaseName: string;
}
export interface AzureResourceDatabase {
name: string;
serverName: string;
serverFullName: string;
loginName: string;
}
}

View File

@@ -0,0 +1,129 @@
/*---------------------------------------------------------------------------------------------
* 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 { extensions, TreeItem } from 'vscode';
import { Account } from 'sqlops';
import { azureResource } from './azure-resource';
import { IAzureResourceNodeWithProviderId } from './interfaces';
export class AzureResourceService {
private constructor() {
}
public static getInstance(): AzureResourceService {
return AzureResourceService._instance;
}
public async listResourceProviderIds(): Promise<string[]> {
await this.ensureResourceProvidersRegistered();
return Object.keys(this._resourceProviders);
}
public registerResourceProvider(resourceProvider: azureResource.IAzureResourceProvider): void {
this.doRegisterResourceProvider(resourceProvider);
}
public clearResourceProviders(): void {
this._resourceProviders = {};
this._treeDataProviders = {};
this._areResourceProvidersLoaded = false;
}
public async getRootChildren(resourceProviderId: string, account: Account, subscription: azureResource.AzureResourceSubscription, tenatId: string): Promise<IAzureResourceNodeWithProviderId[]> {
await this.ensureResourceProvidersRegistered();
if (!(resourceProviderId in this._resourceProviders)) {
throw new Error(`Azure resource provider doesn't exist. Id: ${resourceProviderId}`);
}
const treeDataProvider = this._treeDataProviders[resourceProviderId];
const children = await treeDataProvider.getChildren();
return children.map((child) => <IAzureResourceNodeWithProviderId>{
resourceProviderId: resourceProviderId,
resourceNode: <azureResource.IAzureResourceNode>{
account: account,
subscription: subscription,
tenantId: tenatId,
treeItem: child.treeItem
}
});
}
public async getChildren(resourceProviderId: string, element: azureResource.IAzureResourceNode): Promise<IAzureResourceNodeWithProviderId[]> {
await this.ensureResourceProvidersRegistered();
if (!(resourceProviderId in this._resourceProviders)) {
throw new Error(`Azure resource provider doesn't exist. Id: ${resourceProviderId}`);
}
const treeDataProvider = this._treeDataProviders[resourceProviderId];
const children = await treeDataProvider.getChildren(element);
return children.map((child) => <IAzureResourceNodeWithProviderId>{
resourceProviderId: resourceProviderId,
resourceNode: child
});
}
public async getTreeItem(resourceProviderId: string, element?: azureResource.IAzureResourceNode): Promise<TreeItem> {
await this.ensureResourceProvidersRegistered();
if (!(resourceProviderId in this._resourceProviders)) {
throw new Error(`Azure resource provider doesn't exist. Id: ${resourceProviderId}`);
}
const treeDataProvider = this._treeDataProviders[resourceProviderId];
return treeDataProvider.getTreeItem(element);
}
public get areResourceProvidersLoaded(): boolean {
return this._areResourceProvidersLoaded;
}
public set areResourceProvidersLoaded(value: boolean) {
this._areResourceProvidersLoaded = value;
}
private async ensureResourceProvidersRegistered(): Promise<void> {
if (this._areResourceProvidersLoaded) {
return;
}
for (const extension of extensions.all) {
const contributes = extension.packageJSON && extension.packageJSON.contributes;
if (!contributes) {
continue;
}
if (contributes['hasAzureResourceProviders']) {
await extension.activate();
if (extension.exports && extension.exports.provideResources) {
for (const resourceProvider of <azureResource.IAzureResourceProvider[]>extension.exports.provideResources()) {
this.doRegisterResourceProvider(resourceProvider);
}
}
}
}
this._areResourceProvidersLoaded = true;
}
private doRegisterResourceProvider(resourceProvider: azureResource.IAzureResourceProvider): void {
this._resourceProviders[resourceProvider.providerId] = resourceProvider;
this._treeDataProviders[resourceProvider.providerId] = resourceProvider.getTreeDataProvider();
}
private _areResourceProvidersLoaded: boolean = false;
private _resourceProviders: { [resourceProviderId: string]: azureResource.IAzureResourceProvider } = {};
private _treeDataProviders: { [resourceProviderId: string]: azureResource.IAzureResourceTreeDataProvider } = {};
private static readonly _instance = new AzureResourceService();
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { NodeInfo } from 'sqlops';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { TreeNode } from './treeNode';
import { AzureResourceService } from './resourceService';
import { IAzureResourceNodeWithProviderId } from './interfaces';
import { AzureResourceMessageTreeNode } from './messageTreeNode';
import { AzureResourceErrorMessageUtil } from './utils';
export class AzureResourceResourceTreeNode extends TreeNode {
public constructor(
public readonly resourceNodeWithProviderId: IAzureResourceNodeWithProviderId,
parent: TreeNode
) {
super();
this.parent = parent;
}
public async getChildren(): Promise<TreeNode[]> {
// It is a leaf node.
if (this.resourceNodeWithProviderId.resourceNode.treeItem.collapsibleState === TreeItemCollapsibleState.None) {
return <TreeNode[]>[];
}
try {
const children = await this._resourceService.getChildren(this.resourceNodeWithProviderId.resourceProviderId, this.resourceNodeWithProviderId.resourceNode);
if (children.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceResourceTreeNode.noResourcesLabel, this)];
} else {
return children.map((child) => {
// To make tree node's id unique, otherwise, treeModel.js would complain 'item already registered'
child.resourceNode.treeItem.id = `${this.resourceNodeWithProviderId.resourceNode.treeItem.id}.${child.resourceNode.treeItem.id}`;
return new AzureResourceResourceTreeNode(child, this);
});
}
} catch (error) {
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
}
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
return this._resourceService.getTreeItem(this.resourceNodeWithProviderId.resourceProviderId, this.resourceNodeWithProviderId.resourceNode);
}
public getNodeInfo(): NodeInfo {
const treeItem = this.resourceNodeWithProviderId.resourceNode.treeItem;
return {
label: treeItem.label,
isLeaf: treeItem.collapsibleState === TreeItemCollapsibleState.None ? true : false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: treeItem.contextValue,
nodeSubType: undefined,
iconType: treeItem.contextValue
};
}
public get nodePathValue(): string {
return this.resourceNodeWithProviderId.resourceNode.treeItem.id;
}
private _resourceService = AzureResourceService.getInstance();
private static readonly noResourcesLabel = localize('azure.resource.resourceTreeNode.noResourcesLabel', 'No Resources found.');
}

View File

@@ -1,35 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 {
IAzureResourceAccountService,
IAzureResourceCredentialService,
IAzureResourceSubscriptionService,
IAzureResourceSubscriptionFilterService,
IAzureResourceDatabaseService,
IAzureResourceDatabaseServerService,
IAzureResourceCacheService,
IAzureResourceContextService } from './interfaces';
export class AzureResourceServicePool {
private constructor() { }
public static getInstance(): AzureResourceServicePool {
return AzureResourceServicePool._instance;
}
public contextService: IAzureResourceContextService;
public cacheService: IAzureResourceCacheService;
public accountService: IAzureResourceAccountService;
public credentialService: IAzureResourceCredentialService;
public subscriptionService: IAzureResourceSubscriptionService;
public subscriptionFilterService: IAzureResourceSubscriptionFilterService;
public databaseService: IAzureResourceDatabaseService;
public databaseServerService: IAzureResourceDatabaseServerService;
private static readonly _instance = new AzureResourceServicePool();
}

View File

@@ -5,21 +5,30 @@
'use strict';
import { ExtensionContext } from "vscode";
import { ExtensionContext } from 'vscode';
import { IAzureResourceCacheService } from "../interfaces";
import { IAzureResourceCacheService } from '../interfaces';
export class AzureResourceCacheService implements IAzureResourceCacheService {
public constructor(
public readonly context: ExtensionContext
context: ExtensionContext
) {
this._context = context;
}
public get<T>(key: string): T | undefined {
return this.context.workspaceState.get(key);
public generateKey(id: string): string {
return `${AzureResourceCacheService.cacheKeyPrefix}.${id}`;
}
public get<T>(key: string): T | undefined {
return this._context.workspaceState.get(key);
}
public update<T>(key: string, value: T): void {
this.context.workspaceState.update(key, value);
this._context.workspaceState.update(key, value);
}
private _context: ExtensionContext = undefined;
private static readonly cacheKeyPrefix = 'azure.resource.cache';
}

View File

@@ -1,36 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ExtensionContext } from "vscode";
import { ApiWrapper } from "../../apiWrapper";
import { IAzureResourceContextService } from "../interfaces";
export class AzureResourceContextService implements IAzureResourceContextService {
public constructor(
context: ExtensionContext,
apiWrapper: ApiWrapper
) {
this._context = context;
this._apiWrapper = apiWrapper;
}
public getAbsolutePath(relativePath: string): string {
return this._context.asAbsolutePath(relativePath);
}
public executeCommand(commandId: string, ...args: any[]): void {
this._apiWrapper.executeCommand(commandId, args);
}
public showErrorMessage(errorMessage: string): void {
this._apiWrapper.showErrorMessage(errorMessage);
}
private _context: ExtensionContext = undefined;
private _apiWrapper: ApiWrapper = undefined;
}

View File

@@ -1,43 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { TokenCredentials, ServiceClientCredentials } from 'ms-rest';
import { ApiWrapper } from '../../apiWrapper';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { IAzureResourceCredentialService } from '../interfaces';
import { AzureResourceCredentialError } from '../errors';
export class AzureResourceCredentialService implements IAzureResourceCredentialService {
public constructor(
apiWrapper: ApiWrapper
) {
this._apiWrapper = apiWrapper;
}
public async getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise<ServiceClientCredentials[]> {
try {
let credentials: TokenCredentials[] = [];
let tokens = await this._apiWrapper.getSecurityToken(account, resource);
for (let tenant of account.properties.tenants) {
let token = tokens[tenant.id].token;
let tokenType = tokens[tenant.id].tokenType;
credentials.push(new TokenCredentials(token, tokenType));
}
return credentials;
} catch (error) {
throw new AzureResourceCredentialError(localize('azureResource.services.credentialService.credentialError', 'Failed to get credential for account {0}. Please refresh the account.', account.key.accountId), error);
}
}
private _apiWrapper: ApiWrapper = undefined;
}

View File

@@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ServiceClientCredentials } from 'ms-rest';
import { SqlManagementClient } from 'azure-arm-sql';
import { IAzureResourceDatabaseServerService } from '../interfaces';
import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../models';
export class AzureResourceDatabaseServerService implements IAzureResourceDatabaseServerService {
public async getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise<AzureResourceDatabaseServer[]> {
let databaseServers: AzureResourceDatabaseServer[] = [];
for (let cred of credentials) {
let sqlManagementClient = new SqlManagementClient(cred, subscription.id);
try {
let svrs = await sqlManagementClient.servers.list();
svrs.forEach((svr) => databaseServers.push({
name: svr.name,
fullName: svr.fullyQualifiedDomainName,
loginName: svr.administratorLogin,
defaultDatabaseName: 'master'
}));
} catch (error) {
if (error.code === 'InvalidAuthenticationTokenTenant' && error.statusCode === 401) {
/**
* There may be multiple tenants for an account and it may throw exceptions like following. Just swallow the exception here.
* The access token is from the wrong issuer. It must match one of the tenants associated with this subscription.
*/
}
}
}
return databaseServers;
}
}

View File

@@ -1,51 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ServiceClientCredentials } from 'ms-rest';
import { SqlManagementClient } from 'azure-arm-sql';
import { IAzureResourceDatabaseService } from '../interfaces';
import { AzureResourceSubscription, AzureResourceDatabase } from '../models';
export class AzureResourceDatabaseService implements IAzureResourceDatabaseService {
public async getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise<AzureResourceDatabase[]> {
let databases: AzureResourceDatabase[] = [];
for (let cred of credentials) {
let sqlManagementClient = new SqlManagementClient(cred, subscription.id);
try {
let svrs = await sqlManagementClient.servers.list();
for (let svr of svrs) {
// Extract resource group name from svr.id
let svrIdRegExp = new RegExp(`\/subscriptions\/${subscription.id}\/resourceGroups\/(.+)\/providers\/Microsoft\.Sql\/servers\/${svr.name}`);
if (!svrIdRegExp.test(svr.id)) {
continue;
}
let founds = svrIdRegExp.exec(svr.id);
let resouceGroup = founds[1];
let dbs = await sqlManagementClient.databases.listByServer(resouceGroup, svr.name);
dbs.forEach((db) => databases.push({
name: db.name,
serverName: svr.name,
serverFullName: svr.fullyQualifiedDomainName,
loginName: svr.administratorLogin
}));
}
} catch (error) {
if (error.code === 'InvalidAuthenticationTokenTenant' && error.statusCode === 401) {
/**
* There may be multiple tenants for an account and it may throw exceptions like following. Just swallow the exception here.
* The access token is from the wrong issuer. It must match one of the tenants associated with this subscription.
*/
}
}
}
return databases;
}
}

View File

@@ -8,11 +8,11 @@
import { WorkspaceConfiguration, ConfigurationTarget } from 'vscode';
import { Account } from 'sqlops';
import { azureResource } from '../azure-resource';
import { IAzureResourceSubscriptionFilterService, IAzureResourceCacheService } from '../interfaces';
import { AzureResourceSubscription } from '../models';
interface AzureResourceSelectedSubscriptionsCache {
selectedSubscriptions: { [accountId: string]: AzureResourceSubscription[]};
selectedSubscriptions: { [accountId: string]: azureResource.AzureResourceSubscription[]};
}
export class AzureResourceSubscriptionFilterService implements IAzureResourceSubscriptionFilterService {
@@ -20,12 +20,14 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub
cacheService: IAzureResourceCacheService
) {
this._cacheService = cacheService;
this._cacheKey = this._cacheService.generateKey('selectedSubscriptions');
}
public async getSelectedSubscriptions(account: Account): Promise<AzureResourceSubscription[]> {
let selectedSubscriptions: AzureResourceSubscription[] = [];
public async getSelectedSubscriptions(account: Account): Promise<azureResource.AzureResourceSubscription[]> {
let selectedSubscriptions: azureResource.AzureResourceSubscription[] = [];
const cache = this._cacheService.get<AzureResourceSelectedSubscriptionsCache>(AzureResourceSubscriptionFilterService.CacheKey);
const cache = this._cacheService.get<AzureResourceSelectedSubscriptionsCache>(this._cacheKey);
if (cache) {
selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId];
}
@@ -33,10 +35,10 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub
return selectedSubscriptions;
}
public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise<void> {
let selectedSubscriptionsCache: { [accountId: string]: AzureResourceSubscription[]} = {};
public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise<void> {
let selectedSubscriptionsCache: { [accountId: string]: azureResource.AzureResourceSubscription[]} = {};
const cache = this._cacheService.get<AzureResourceSelectedSubscriptionsCache>(AzureResourceSubscriptionFilterService.CacheKey);
const cache = this._cacheService.get<AzureResourceSelectedSubscriptionsCache>(this._cacheKey);
if (cache) {
selectedSubscriptionsCache = cache.selectedSubscriptions;
}
@@ -47,14 +49,14 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub
selectedSubscriptionsCache[account.key.accountId] = selectedSubscriptions;
this._cacheService.update<AzureResourceSelectedSubscriptionsCache>(AzureResourceSubscriptionFilterService.CacheKey, { selectedSubscriptions: selectedSubscriptionsCache });
this._cacheService.update<AzureResourceSelectedSubscriptionsCache>(this._cacheKey, { selectedSubscriptions: selectedSubscriptionsCache });
const filters: string[] = [];
for (const accountId in selectedSubscriptionsCache) {
filters.push(...selectedSubscriptionsCache[accountId].map((subcription) => `${accountId}/${subcription.id}/${subcription.name}`));
}
const resourceFilterConfig = this._config.inspect<string[]>(AzureResourceSubscriptionFilterService.FilterConfigName);
const resourceFilterConfig = this._config.inspect<string[]>(AzureResourceSubscriptionFilterService.filterConfigName);
let configTarget = ConfigurationTarget.Global;
if (resourceFilterConfig) {
if (resourceFilterConfig.workspaceFolderValue) {
@@ -66,12 +68,12 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub
}
}
await this._config.update(AzureResourceSubscriptionFilterService.FilterConfigName, filters, configTarget);
await this._config.update(AzureResourceSubscriptionFilterService.filterConfigName, filters, configTarget);
}
private _config: WorkspaceConfiguration = undefined;
private _cacheService: IAzureResourceCacheService = undefined;
private _cacheKey: string = undefined;
private static readonly FilterConfigName = 'resourceFilter';
private static readonly CacheKey = 'azureResource.cache.selectedSubscriptions';
private static readonly filterConfigName = 'azure.resource.config.filter';
}

View File

@@ -9,24 +9,19 @@ import { Account } from 'sqlops';
import { ServiceClientCredentials } from 'ms-rest';
import { SubscriptionClient } from 'azure-arm-resource';
import { azureResource } from '../azure-resource';
import { IAzureResourceSubscriptionService } from '../interfaces';
import { AzureResourceSubscription } from '../models';
export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService {
public async getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise<AzureResourceSubscription[]> {
let subscriptions: AzureResourceSubscription[] = [];
for (let cred of credentials) {
let subClient = new SubscriptionClient.SubscriptionClient(cred);
try {
let subs = await subClient.subscriptions.list();
subs.forEach((sub) => subscriptions.push({
id: sub.subscriptionId,
name: sub.displayName
}));
} catch (error) {
// Swallow the exception here.
}
}
public async getSubscriptions(account: Account, credential: ServiceClientCredentials): Promise<azureResource.AzureResourceSubscription[]> {
const subscriptions: azureResource.AzureResourceSubscription[] = [];
const subClient = new SubscriptionClient.SubscriptionClient(credential);
const subs = await subClient.subscriptions.list();
subs.forEach((sub) => subscriptions.push({
id: sub.subscriptionId,
name: sub.displayName
}));
return subscriptions;
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as request from 'request';
import { azureResource } from '../azure-resource';
import { IAzureResourceTenantService } from '../interfaces';
export class AzureResourceTenantService implements IAzureResourceTenantService {
public async getTenantId(subscription: azureResource.AzureResourceSubscription): Promise<string> {
const requestPromisified = new Promise<string>((resolve, reject) => {
const url = `https://management.azure.com/subscriptions/${subscription.id}?api-version=2014-04-01`;
request(url, function (error, response, body) {
if (response.statusCode === 401) {
const tenantIdRegEx = /authorization_uri="https:\/\/login\.windows\.net\/([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})"/;
const teantIdString = response.headers['www-authenticate'];
if (tenantIdRegEx.test(teantIdString)) {
resolve(tenantIdRegEx.exec(teantIdString)[1]);
} else {
reject();
}
}
});
});
return await requestPromisified;
}
}

View File

@@ -7,10 +7,10 @@
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { TreeNode } from '../treeNode';
import { AzureResourceItemType } from '../constants';
export class AzureResourceAccountNotSignedInTreeNode extends TreeNode {
@@ -19,11 +19,11 @@ export class AzureResourceAccountNotSignedInTreeNode extends TreeNode {
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.SignInLabel, TreeItemCollapsibleState.None);
let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.signInLabel, TreeItemCollapsibleState.None);
item.contextValue = AzureResourceItemType.message;
item.command = {
title: AzureResourceAccountNotSignedInTreeNode.SignInLabel,
command: 'azureresource.signin',
title: AzureResourceAccountNotSignedInTreeNode.signInLabel,
command: 'azure.resource.signin',
arguments: [this]
};
return item;
@@ -31,7 +31,7 @@ export class AzureResourceAccountNotSignedInTreeNode extends TreeNode {
public getNodeInfo(): NodeInfo {
return {
label: AzureResourceAccountNotSignedInTreeNode.SignInLabel,
label: AzureResourceAccountNotSignedInTreeNode.signInLabel,
isLeaf: true,
errorMessage: undefined,
metadata: undefined,
@@ -47,5 +47,5 @@ export class AzureResourceAccountNotSignedInTreeNode extends TreeNode {
return 'message_accountNotSignedIn';
}
private static readonly SignInLabel = localize('azureResource.tree.accountNotSignedInTreeNode.signIn', 'Sign in to Azure ...');
private static readonly signInLabel = localize('azure.resource.tree.accountNotSignedInTreeNode.signInLabel', 'Sign in to Azure ...');
}

View File

@@ -6,44 +6,59 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Account, NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import { Account, NodeInfo, AzureResource } from 'sqlops';
import { TokenCredentials } from 'ms-rest';
import { AppContext } from '../../appContext';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { azureResource } from '../azure-resource';
import { TreeNode } from '../treeNode';
import { AzureResourceCredentialError } from '../errors';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType } from '../constants';
import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
import { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode';
import { AzureResourceMessageTreeNode } from './messageTreeNode';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';
import { AzureResourceSubscription } from '../models';
import { IAzureResourceTreeChangeHandler } from './treeProvider';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceTenantService } from '../../azureResource/interfaces';
export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(
account: Account,
public readonly account: Account,
appContext: AppContext,
treeChangeHandler: IAzureResourceTreeChangeHandler
) {
super(account, treeChangeHandler, undefined);
super(appContext, treeChangeHandler, undefined);
this._subscriptionService = this.appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
this._subscriptionFilterService = this.appContext.getService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
this._tenantService = this.appContext.getService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService);
this._id = `account_${this.account.key.accountId}`;
this.setCacheKey(`${this._id}.subscriptions`);
this._label = this.generateLabel();
}
public async getChildren(): Promise<TreeNode[]> {
try {
let subscriptions: AzureResourceSubscription[] = [];
let subscriptions: azureResource.AzureResourceSubscription[] = [];
if (this._isClearingCache) {
const credentials = await this.getCredentials();
subscriptions = (await this.servicePool.subscriptionService.getSubscriptions(this.account, credentials)) || <AzureResourceSubscription[]>[];
try {
const tokens = await this.appContext.apiWrapper.getSecurityToken(this.account, AzureResource.ResourceManagement);
let cache = this.getCache<AzureResourceSubscriptionsCache>();
if (!cache) {
cache = { subscriptions: { } };
for (const tenant of this.account.properties.tenants) {
const token = tokens[tenant.id].token;
const tokenType = tokens[tenant.id].tokenType;
subscriptions.push(...(await this._subscriptionService.getSubscriptions(this.account, new TokenCredentials(token, tokenType)) || <azureResource.AzureResourceSubscription[]>[]));
}
} catch (error) {
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', 'Failed to get credential for account {0}. Please refresh the account.', this.account.key.accountId), error);
}
cache.subscriptions[this.account.key.accountId] = subscriptions;
this.updateCache<AzureResourceSubscriptionsCache>(cache);
this.updateCache<azureResource.AzureResourceSubscription[]>(subscriptions);
this._isClearingCache = false;
} else {
@@ -52,8 +67,8 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
this._totalSubscriptionCount = subscriptions.length;
let selectedSubscriptions = await this.servicePool.subscriptionFilterService.getSelectedSubscriptions(this.account);
let selectedSubscriptionIds = (selectedSubscriptions || <AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
const selectedSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account);
const selectedSubscriptionIds = (selectedSubscriptions || <azureResource.AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
if (selectedSubscriptionIds.length > 0) {
subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
this._selectedSubscriptionCount = selectedSubscriptionIds.length;
@@ -65,31 +80,36 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
this.refreshLabel();
if (subscriptions.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.NoSubscriptions, this)];
return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noSubscriptionsLabel, this)];
} else {
return subscriptions.map((subscription) => new AzureResourceSubscriptionTreeNode(subscription, this.account, this.treeChangeHandler, this));
return await Promise.all(subscriptions.map(async (subscription) => {
const tenantId = await this._tenantService.getTenantId(subscription);
return new AzureResourceSubscriptionTreeNode(this.account, subscription, tenantId, this.appContext, this.treeChangeHandler, this);
}));
}
} catch (error) {
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
if (error instanceof AzureResourceCredentialError) {
this.appContext.apiWrapper.showErrorMessage(error.message);
this.appContext.apiWrapper.executeCommand('azure.resource.signin');
} else {
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
}
}
}
public async getCachedSubscriptions(): Promise<AzureResourceSubscription[]> {
const subscriptions: AzureResourceSubscription[] = [];
const cache = this.getCache<AzureResourceSubscriptionsCache>();
if (cache) {
subscriptions.push(...cache.subscriptions[this.account.key.accountId]);
}
return subscriptions;
public async getCachedSubscriptions(): Promise<azureResource.AzureResourceSubscription[]> {
return this.getCache<azureResource.AzureResourceSubscription[]>();
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
let item = new TreeItem(this._label, TreeItemCollapsibleState.Collapsed);
const item = new TreeItem(this._label, TreeItemCollapsibleState.Collapsed);
item.id = this._id;
item.contextValue = AzureResourceItemType.account;
item.iconPath = {
dark: this.servicePool.contextService.getAbsolutePath('resources/dark/account_inverse.svg'),
light: this.servicePool.contextService.getAbsolutePath('resources/light/account.svg')
dark: this.appContext.extensionContext.asAbsolutePath('resources/dark/account_inverse.svg'),
light: this.appContext.extensionContext.asAbsolutePath('resources/light/account.svg')
};
return item;
}
@@ -128,10 +148,6 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
}
}
protected get cacheKey(): string {
return 'azureResource.cache.subscriptions';
}
private generateLabel(): string {
let label = `${this.account.displayInfo.displayName} (${this.account.key.accountId})`;
@@ -142,14 +158,14 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
return label;
}
private _subscriptionService: IAzureResourceSubscriptionService = undefined;
private _subscriptionFilterService: IAzureResourceSubscriptionFilterService = undefined;
private _tenantService: IAzureResourceTenantService = undefined;
private _id: string = undefined;
private _label: string = undefined;
private _totalSubscriptionCount = 0;
private _selectedSubscriptionCount = 0;
private static readonly NoSubscriptions = localize('azureResource.tree.accountTreeNode.noSubscriptions', 'No Subscriptions found.');
}
interface AzureResourceSubscriptionsCache {
subscriptions: { [accountId: string]: AzureResourceSubscription[] };
}
private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', 'No Subscriptions found.');
}

View File

@@ -5,16 +5,16 @@
'use strict';
import * as sqlops from 'sqlops';
import { ServiceClientCredentials } from 'ms-rest';
import { TreeNode } from '../../treeNodes';
import { AppContext } from '../../appContext';
import { AzureResourceServicePool } from '../servicePool';
import { AzureResourceCredentialError } from '../errors';
import { TreeNode } from '../treeNode';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceCacheService } from '../../azureResource/interfaces';
import { AzureResourceServiceNames } from '../constants';
export abstract class AzureResourceTreeNodeBase extends TreeNode {
public constructor(
public readonly appContext: AppContext,
public readonly treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
@@ -22,17 +22,17 @@ export abstract class AzureResourceTreeNodeBase extends TreeNode {
this.parent = parent;
}
public readonly servicePool = AzureResourceServicePool.getInstance();
}
export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase {
public constructor(
public readonly account: sqlops.Account,
appContext: AppContext,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
super(treeChangeHandler, parent);
super(appContext, treeChangeHandler, parent);
this._cacheService = this.appContext.getService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService);
}
public clearCache(): void {
@@ -43,29 +43,19 @@ export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTr
return this._isClearingCache;
}
protected async getCredentials(): Promise<ServiceClientCredentials[]> {
try {
return await this.servicePool.credentialService.getCredentials(this.account, sqlops.AzureResource.ResourceManagement);
} catch (error) {
if (error instanceof AzureResourceCredentialError) {
this.servicePool.contextService.showErrorMessage(error.message);
this.servicePool.contextService.executeCommand('azureresource.signin');
} else {
throw error;
}
}
}
protected setCacheKey(id: string): void {
this._cacheKey = this._cacheService.generateKey(id);
}
protected updateCache<T>(cache: T): void {
this.servicePool.cacheService.update<T>(this.cacheKey, cache);
this._cacheService.update<T>(this._cacheKey, cache);
}
protected getCache<T>(): T {
return this.servicePool.cacheService.get<T>(this.cacheKey);
return this._cacheService.get<T>(this._cacheKey);
}
protected abstract get cacheKey(): string;
protected _isClearingCache = true;
private _cacheService: IAzureResourceCacheService = undefined;
private _cacheKey: string = undefined;
}

View File

@@ -1,103 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Account, NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType } from '../constants';
import { AzureResourceErrorMessageUtil } from '../utils';
import { AzureResourceDatabaseTreeNode } from './databaseTreeNode';
import { AzureResourceMessageTreeNode } from './messageTreeNode';
import { AzureResourceSubscription, AzureResourceDatabase } from '../models';
import { IAzureResourceTreeChangeHandler } from './treeProvider';
export class AzureResourceDatabaseContainerTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(
public readonly subscription: AzureResourceSubscription,
account: Account,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
super(account, treeChangeHandler, parent);
}
public async getChildren(): Promise<TreeNode[]> {
try {
let databases: AzureResourceDatabase[] = [];
if (this._isClearingCache) {
let credentials = await this.getCredentials();
databases = (await this.servicePool.databaseService.getDatabases(this.subscription, credentials)) || <AzureResourceDatabase[]>[];
let cache = this.getCache<AzureResourceDatabasesCache>();
if (!cache) {
cache = { databases: { } };
}
cache.databases[this.subscription.id] = databases;
this.updateCache(cache);
this._isClearingCache = false;
} else {
const cache = this.getCache<AzureResourceDatabasesCache>();
if (cache) {
databases = cache.databases[this.subscription.id] || <AzureResourceDatabase[]>[];
}
}
if (databases.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceDatabaseContainerTreeNode.NoDatabases, this)];
} else {
return databases.map((database) => new AzureResourceDatabaseTreeNode(database, this.treeChangeHandler, this));
}
} catch (error) {
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
}
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
let item = new TreeItem(AzureResourceDatabaseContainerTreeNode.Label, TreeItemCollapsibleState.Collapsed);
item.contextValue = AzureResourceItemType.databaseContainer;
item.iconPath = {
dark: this.servicePool.contextService.getAbsolutePath('resources/dark/folder_inverse.svg'),
light: this.servicePool.contextService.getAbsolutePath('resources/light/folder.svg')
};
return item;
}
public getNodeInfo(): NodeInfo {
return {
label: AzureResourceDatabaseContainerTreeNode.Label,
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: AzureResourceItemType.databaseContainer,
nodeSubType: undefined,
iconType: AzureResourceItemType.databaseContainer
};
}
public get nodePathValue(): string {
return 'databaseContainer';
}
protected get cacheKey(): string {
return 'azureResource.cache.databases';
}
private static readonly Label = localize('azureResource.tree.databaseContainerTreeNode.label', 'SQL Databases');
private static readonly NoDatabases = localize('azureResource.tree.databaseContainerTreeNode.noDatabases', 'No SQL Databases found.');
}
interface AzureResourceDatabasesCache {
databases: { [subscriptionId: string]: AzureResourceDatabase[] };
}

View File

@@ -1,103 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Account, NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType } from '../constants';
import { AzureResourceMessageTreeNode } from './messageTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';
import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../models';
import { AzureResourceDatabaseServerTreeNode } from './databaseServerTreeNode';
import { IAzureResourceTreeChangeHandler } from './treeProvider';
export class AzureResourceDatabaseServerContainerTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(
public readonly subscription: AzureResourceSubscription,
account: Account,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
super(account, treeChangeHandler, parent);
}
public async getChildren(): Promise<TreeNode[]> {
try {
let databaseServers: AzureResourceDatabaseServer[] = [];
if (this._isClearingCache) {
let credentials = await this.getCredentials();
databaseServers = (await this.servicePool.databaseServerService.getDatabaseServers(this.subscription, credentials)) || <AzureResourceDatabaseServer[]>[];
let cache = this.getCache<AzureResourceDatabaseServersCache>();
if (!cache) {
cache = { databaseServers: { } };
}
cache.databaseServers[this.subscription.id] = databaseServers;
this.updateCache<AzureResourceDatabaseServersCache>(cache);
this._isClearingCache = false;
} else {
const cache = this.getCache<AzureResourceDatabaseServersCache>();
if (cache) {
databaseServers = cache.databaseServers[this.subscription.id] || <AzureResourceDatabaseServer[]>[];
}
}
if (databaseServers.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceDatabaseServerContainerTreeNode.NoDatabaseServers, this)];
} else {
return databaseServers.map((server) => new AzureResourceDatabaseServerTreeNode(server, this.treeChangeHandler, this));
}
} catch (error) {
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
}
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
let item = new TreeItem(AzureResourceDatabaseServerContainerTreeNode.Label, TreeItemCollapsibleState.Collapsed);
item.contextValue = AzureResourceItemType.databaseServerContainer;
item.iconPath = {
dark: this.servicePool.contextService.getAbsolutePath('resources/dark/folder_inverse.svg'),
light: this.servicePool.contextService.getAbsolutePath('resources/light/folder.svg')
};
return item;
}
public getNodeInfo(): NodeInfo {
return {
label: AzureResourceDatabaseServerContainerTreeNode.Label,
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: AzureResourceItemType.databaseServerContainer,
nodeSubType: undefined,
iconType: AzureResourceItemType.databaseServerContainer
};
}
public get nodePathValue(): string {
return 'databaseServerContainer';
}
protected get cacheKey(): string {
return 'azureResource.cache.databaseServers';
}
private static readonly Label = localize('azureResource.tree.databaseServerContainerTreeNode.label', 'SQL Servers');
private static readonly NoDatabaseServers = localize('azureResource.tree.databaseContainerTreeNode.noDatabaseServers', 'No SQL Servers found.');
}
interface AzureResourceDatabaseServersCache {
databaseServers: { [subscriptionId: string]: AzureResourceDatabaseServer[] };
}

View File

@@ -1,57 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import { AzureResourceTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType } from '../constants';
import { AzureResourceDatabaseServer } from '../models';
import { IAzureResourceTreeChangeHandler } from './treeProvider';
export class AzureResourceDatabaseServerTreeNode extends AzureResourceTreeNodeBase {
public constructor(
public readonly databaseServer: AzureResourceDatabaseServer,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
super(treeChangeHandler, parent);
}
public async getChildren(): Promise<TreeNode[]> {
return [];
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
let item = new TreeItem(this.databaseServer.name, TreeItemCollapsibleState.None);
item.contextValue = AzureResourceItemType.databaseServer;
item.iconPath = {
dark: this.servicePool.contextService.getAbsolutePath('resources/dark/sql_server_inverse.svg'),
light: this.servicePool.contextService.getAbsolutePath('resources/light/sql_server.svg')
};
return item;
}
public getNodeInfo(): NodeInfo {
return {
label: this.databaseServer.name,
isLeaf: true,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: AzureResourceItemType.databaseServer,
nodeSubType: undefined,
iconType: AzureResourceItemType.databaseServer
};
}
public get nodePathValue(): string {
return `databaseServer_${this.databaseServer.name}`;
}
}

View File

@@ -1,61 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import { AzureResourceTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType } from '../constants';
import { AzureResourceDatabase } from '../models';
import { IAzureResourceTreeChangeHandler } from './treeProvider';
export class AzureResourceDatabaseTreeNode extends AzureResourceTreeNodeBase {
public constructor(
public readonly database: AzureResourceDatabase,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
super(treeChangeHandler, parent);
this._label = `${this.database.name} (${this.database.serverName})`;
}
public async getChildren(): Promise<TreeNode[]> {
return [];
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
let item = new TreeItem(this._label, TreeItemCollapsibleState.None);
item.contextValue = AzureResourceItemType.database;
item.iconPath = {
dark: this.servicePool.contextService.getAbsolutePath('resources/dark/sql_database_inverse.svg'),
light: this.servicePool.contextService.getAbsolutePath('resources/light/sql_database.svg')
};
return item;
}
public getNodeInfo(): NodeInfo {
return {
label: this._label,
isLeaf: true,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: AzureResourceItemType.database,
nodeSubType: undefined,
iconType: AzureResourceItemType.database
};
}
public get nodePathValue(): string {
return `database_${this.database.name}`;
}
private _label: string = undefined;
}

View File

@@ -7,38 +7,66 @@
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Account, NodeInfo } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import { AppContext } from '../../appContext';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { AzureResourceTreeNodeBase, AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { azureResource } from '../azure-resource';
import { TreeNode } from '../treeNode';
import { IAzureResourceNodeWithProviderId } from '../interfaces';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType } from '../constants';
import { AzureResourceDatabaseContainerTreeNode } from './databaseContainerTreeNode';
import { AzureResourceDatabaseServerContainerTreeNode } from './databaseServerContainerTreeNode';
import { AzureResourceSubscription } from '../models';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';
import { AzureResourceService } from '../resourceService';
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase {
export class AzureResourceSubscriptionTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(
public readonly subscription: AzureResourceSubscription,
account: Account,
public readonly account: Account,
public readonly subscription: azureResource.AzureResourceSubscription,
public readonly tenatId: string,
appContext: AppContext,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
super(treeChangeHandler, parent);
super(appContext, treeChangeHandler, parent);
this._children.push(new AzureResourceDatabaseContainerTreeNode(subscription, account, treeChangeHandler, this));
this._children.push(new AzureResourceDatabaseServerContainerTreeNode(subscription, account, treeChangeHandler, this));
this._id = `account_${this.account.key.accountId}.subscription_${this.subscription.id}.tenant_${this.tenatId}`;
this.setCacheKey(`${this._id}.resources`);
}
public async getChildren(): Promise<TreeNode[]> {
return this._children;
try {
const resourceService = AzureResourceService.getInstance();
const children: IAzureResourceNodeWithProviderId[] = [];
for (const resourceProviderId of await resourceService.listResourceProviderIds()) {
children.push(...await resourceService.getRootChildren(resourceProviderId, this.account, this.subscription, this.tenatId));
}
if (children.length === 0) {
return [AzureResourceMessageTreeNode.create(AzureResourceSubscriptionTreeNode.noResourcesLabel, this)];
} else {
return children.map((child) => {
// To make tree node's id unique, otherwise, treeModel.js would complain 'item already registered'
child.resourceNode.treeItem.id = `${this._id}.${child.resourceNode.treeItem.id}`;
return new AzureResourceResourceTreeNode(child, this);
});
}
} catch (error) {
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
}
}
public getTreeItem(): TreeItem | Promise<TreeItem> {
let item = new TreeItem(this.subscription.name, TreeItemCollapsibleState.Collapsed);
const item = new TreeItem(this.subscription.name, TreeItemCollapsibleState.Collapsed);
item.contextValue = AzureResourceItemType.subscription;
item.iconPath = {
dark: this.servicePool.contextService.getAbsolutePath('resources/dark/subscription_inverse.svg'),
light: this.servicePool.contextService.getAbsolutePath('resources/light/subscription.svg')
dark: this.appContext.extensionContext.asAbsolutePath('resources/dark/subscription_inverse.svg'),
light: this.appContext.extensionContext.asAbsolutePath('resources/light/subscription.svg')
};
return item;
}
@@ -58,8 +86,10 @@ export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase
}
public get nodePathValue(): string {
return `subscription_${this.subscription.id}`;
return this._id;
}
private _children: AzureResourceContainerTreeNodeBase[] = [];
private _id: string = undefined;
private static readonly noResourcesLabel = localize('azure.resource.tree.subscriptionTreeNode.noResourcesLabel', 'No Resources found.');
}

View File

@@ -5,7 +5,7 @@
'use strict';
import { TreeNode } from '../../treeNodes';
import { TreeNode } from '../treeNode';
export interface IAzureResourceTreeChangeHandler {
notifyNodeChanged(node: TreeNode): void;

View File

@@ -6,26 +6,25 @@
'use strict';
import { TreeDataProvider, EventEmitter, Event, TreeItem } from 'vscode';
import { DidChangeAccountsParams } from 'sqlops';
import { TreeNode } from '../../treeNodes';
import { setInterval, clearInterval } from 'timers';
import { AppContext } from '../../appContext';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { AzureResourceServicePool } from '../servicePool';
import { TreeNode } from '../treeNode';
import { AzureResourceAccountTreeNode } from './accountTreeNode';
import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode';
import { AzureResourceMessageTreeNode } from './messageTreeNode';
import { AzureResourceContainerTreeNodeBase, AzureResourceTreeNodeBase } from './baseTreeNodes';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil } from '../utils';
export interface IAzureResourceTreeChangeHandler {
notifyNodeChanged(node: TreeNode): void;
}
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceAccountService } from '../../azureResource/interfaces';
import { AzureResourceServiceNames } from '../constants';
export class AzureResourceTreeProvider implements TreeDataProvider<TreeNode>, IAzureResourceTreeChangeHandler {
public constructor() {
AzureResourceServicePool.getInstance().accountService.onDidChangeAccounts((e: DidChangeAccountsParams) => { this._onDidChangeTreeData.fire(undefined); });
public constructor(
public readonly appContext: AppContext
) {
}
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
@@ -37,7 +36,7 @@ export class AzureResourceTreeProvider implements TreeDataProvider<TreeNode>, IA
this._loadingTimer = setInterval(async () => {
try {
// Call sqlops.accounts.getAllAccounts() to determine whether the system has been initialized.
await AzureResourceServicePool.getInstance().accountService.getAccounts();
await this.appContext.getService<IAzureResourceAccountService>(AzureResourceServiceNames.accountService).getAccounts();
// System has been initialized
this.isSystemInitialized = true;
@@ -51,16 +50,16 @@ export class AzureResourceTreeProvider implements TreeDataProvider<TreeNode>, IA
// System not initialized yet
this.isSystemInitialized = false;
}
}, AzureResourceTreeProvider.LoadingTimerInterval);
}, AzureResourceTreeProvider.loadingTimerInterval);
return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.Loading, undefined)];
return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.loadingLabel, undefined)];
}
try {
const accounts = await AzureResourceServicePool.getInstance().accountService.getAccounts();
const accounts = await this.appContext.getService<IAzureResourceAccountService>(AzureResourceServiceNames.accountService).getAccounts();
if (accounts && accounts.length > 0) {
return accounts.map((account) => new AzureResourceAccountTreeNode(account, this));
return accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this));
} else {
return [new AzureResourceAccountNotSignedInTreeNode()];
}
@@ -96,6 +95,6 @@ export class AzureResourceTreeProvider implements TreeDataProvider<TreeNode>, IA
private _loadingTimer: NodeJS.Timer = undefined;
private _onDidChangeTreeData = new EventEmitter<TreeNode>();
private static readonly Loading = localize('azureResource.tree.treeProvider.loading', 'Loading ...');
private static readonly LoadingTimerInterval = 5000;
private static readonly loadingLabel = localize('azure.resource.tree.treeProvider.loadingLabel', 'Loading ...');
private static readonly loadingTimerInterval = 5000;
}

View File

@@ -11,16 +11,6 @@ import * as vscode from 'vscode';
type TreeNodePredicate = (node: TreeNode) => boolean;
export abstract class TreeNode {
private _parent: TreeNode = undefined;
public get parent(): TreeNode {
return this._parent;
}
public set parent(node: TreeNode) {
this._parent = node;
}
public generateNodePath(): string {
let path = undefined;
if (this.parent) {
@@ -65,13 +55,23 @@ export abstract class TreeNode {
return undefined;
}
public get parent(): TreeNode {
return this._parent;
}
public set parent(node: TreeNode) {
this._parent = node;
}
public abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]>;
public abstract getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem>;
public abstract getNodeInfo(): sqlops.NodeInfo;
/**
* The value to use for this node in the node path
*/
public abstract get nodePathValue(): string;
abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]>;
abstract getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem>;
abstract getNodeInfo(): sqlops.NodeInfo;
private _parent: TreeNode = undefined;
}

View File

@@ -12,10 +12,9 @@ export function getErrorMessage(error: Error | string): string {
return (error instanceof Error) ? error.message : error;
}
export class AzureResourceErrorMessageUtil {
public static getErrorMessage(error: Error | string): string {
return localize('azureResource.error', 'Error: {0}', getErrorMessage(error));
return localize('azure.resource.error', 'Error: {0}', getErrorMessage(error));
}
}

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* 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 ControllerBase from './controllerBase';
import { DidChangeAccountsParams } from 'sqlops';
import {
IAzureResourceCacheService,
IAzureResourceAccountService,
IAzureResourceSubscriptionService,
IAzureResourceSubscriptionFilterService,
IAzureResourceTenantService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from '../azureResource/constants';
import { AzureResourceTreeProvider } from '../azureResource/tree/treeProvider';
import { registerAzureResourceCommands } from '../azureResource/commands';
import { AzureResourceAccountService } from '../azureResource/services/accountService';
import { AzureResourceSubscriptionService } from '../azureResource/services/subscriptionService';
import { AzureResourceSubscriptionFilterService } from '../azureResource/services/subscriptionFilterService';
import { AzureResourceCacheService } from '../azureResource/services/cacheService';
import { AzureResourceTenantService } from '../azureResource/services/tenantService';
import { registerAzureResourceDatabaseServerCommands } from '../azureResource/providers/databaseServer/commands';
import { registerAzureResourceDatabaseCommands } from '../azureResource/providers/database/commands';
export default class AzureResourceController extends ControllerBase {
public activate(): Promise<boolean> {
this.appContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, new AzureResourceCacheService(this.extensionContext));
this.appContext.registerService<IAzureResourceAccountService>(AzureResourceServiceNames.accountService, new AzureResourceAccountService(this.apiWrapper));
this.appContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService());
this.appContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(this.extensionContext)));
this.appContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, new AzureResourceTenantService());
const azureResourceTree = new AzureResourceTreeProvider(this.appContext);
this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
this.appContext.getService<IAzureResourceAccountService>(AzureResourceServiceNames.accountService).onDidChangeAccounts((e: DidChangeAccountsParams) => { azureResourceTree.notifyNodeChanged(undefined); });
registerAzureResourceCommands(this.appContext, azureResourceTree);
registerAzureResourceDatabaseServerCommands(this.appContext);
registerAzureResourceDatabaseCommands(this.appContext);
return Promise.resolve(true);
}
public deactivate(): void {
}
}

View File

@@ -1,54 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 ControllerBase from './controllerBase';
import { AzureResourceTreeProvider } from '../azureResource/tree/treeProvider';
import { registerAzureResourceCommands } from '../azureResource/commands';
import { AzureResourceServicePool } from '../azureResource/servicePool';
import { AzureResourceCredentialService } from '../azureResource/services/credentialService';
import { AzureResourceAccountService } from '../azureResource/services/accountService';
import { AzureResourceSubscriptionService } from '../azureResource/services/subscriptionService';
import { AzureResourceSubscriptionFilterService } from '../azureResource/services/subscriptionFilterService';
import { AzureResourceDatabaseServerService } from '../azureResource/services/databaseServerService';
import { AzureResourceDatabaseService } from '../azureResource/services/databaseService';
import { AzureResourceCacheService } from '../azureResource/services/cacheService';
import { AzureResourceContextService } from '../azureResource/services/contextService';
/**
* The main controller class that initializes the extension
*/
export default class MainController extends ControllerBase {
// PUBLIC METHODS //////////////////////////////////////////////////////
/**
* Deactivates the extension
*/
public deactivate(): void {
}
public activate(): Promise<boolean> {
this.configureAzureResource();
return Promise.resolve(true);
}
private configureAzureResource(): void {
let servicePool = AzureResourceServicePool.getInstance();
servicePool.cacheService = new AzureResourceCacheService(this.extensionContext);
servicePool.contextService = new AzureResourceContextService(this.extensionContext, this.apiWrapper);
servicePool.accountService = new AzureResourceAccountService(this.apiWrapper);
servicePool.credentialService = new AzureResourceCredentialService(this.apiWrapper);
servicePool.subscriptionService = new AzureResourceSubscriptionService();
servicePool.subscriptionFilterService = new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(this.extensionContext));
servicePool.databaseService = new AzureResourceDatabaseService();
servicePool.databaseServerService = new AzureResourceDatabaseServerService();
let azureResourceTree = new AzureResourceTreeProvider();
this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
registerAzureResourceCommands(this.apiWrapper, azureResourceTree);
}
}

View File

@@ -6,12 +6,17 @@ import * as path from 'path';
import * as os from 'os';
import * as constants from './constants';
import MainController from './controllers/mainController';
import AzureResourceController from './controllers/azureResourceController';
import { AppContext } from './appContext';
import ControllerBase from './controllers/controllerBase';
import { ApiWrapper } from './apiWrapper';
import { AzureAccountProviderService } from './account-provider/azureAccountProviderService';
import { AzureResourceDatabaseServerProvider } from './azureResource/providers/databaseServer/databaseServerProvider';
import { AzureResourceDatabaseServerService } from './azureResource/providers/databaseServer/databaseServerService';
import { AzureResourceDatabaseProvider } from './azureResource/providers/database/databaseProvider';
import { AzureResourceDatabaseService } from './azureResource/providers/database/databaseService';
let controllers: ControllerBase[] = [];
@@ -35,7 +40,8 @@ export function getDefaultLogLocation() {
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(extensionContext: vscode.ExtensionContext) {
let appContext = new AppContext(extensionContext, new ApiWrapper());
const apiWrapper = new ApiWrapper();
let appContext = new AppContext(extensionContext, apiWrapper);
let activations: Thenable<boolean>[] = [];
// Create the folder for storing the token caches
@@ -56,21 +62,19 @@ export function activate(extensionContext: vscode.ExtensionContext) {
extensionContext.subscriptions.push(accountProviderService);
accountProviderService.activate();
// Start the main controller
let mainController = new MainController(appContext);
controllers.push(mainController);
extensionContext.subscriptions.push(mainController);
activations.push(mainController.activate());
const azureResourceController = new AzureResourceController(appContext);
controllers.push(azureResourceController);
extensionContext.subscriptions.push(azureResourceController);
activations.push(azureResourceController.activate());
return Promise.all(activations)
.then((results: boolean[]) => {
for (let result of results) {
if (!result) {
return false;
}
}
return true;
});
return {
provideResources() {
return [
new AzureResourceDatabaseServerProvider(new AzureResourceDatabaseServerService(), apiWrapper, extensionContext),
new AzureResourceDatabaseProvider(new AzureResourceDatabaseService(), apiWrapper, extensionContext)
];
}
};
}
// this method is called when your extension is deactivated

View File

@@ -9,8 +9,8 @@ import * as should from 'should';
import * as vscode from 'vscode';
import 'mocha';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode';
import { AzureResourceItemType } from '../../azureResource/constants';
import { AzureResourceMessageTreeNode } from '../../azureResource/messageTreeNode';
describe('AzureResourceMessageTreeNode.info', function(): void {
it('Should be correct when created.', async function(): Promise<void> {

View File

@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import 'mocha';
import { azureResource } from '../../../../azureResource/azure-resource';
import { ApiWrapper } from '../../../../apiWrapper';
import { IAzureResourceDatabaseService } from '../../../../azureResource/providers/database/interfaces';
import { AzureResourceDatabaseTreeDataProvider } from '../../../../azureResource/providers/database/databaseTreeDataProvider';
import { AzureResourceDatabase } from '../../../../azureResource/providers/database/models';
import { AzureResourceItemType } from '../../../../azureResource/constants';
// Mock services
let mockDatabaseService: TypeMoq.IMock<IAzureResourceDatabaseService>;
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
// Mock test data
const mockAccount: sqlops.Account = {
key: {
accountId: 'mock_account',
providerId: 'mock_provider'
},
displayInfo: {
displayName: 'mock_account@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test'
},
properties: undefined,
isStale: false
};
const mockSubscription: azureResource.AzureResourceSubscription = {
id: 'mock_subscription',
name: 'mock subscription'
};
const mockTenantId: string = 'mock_tenant';
const mockResourceRootNode: azureResource.IAzureResourceNode = {
account: mockAccount,
subscription: mockSubscription,
tenantId: mockTenantId,
treeItem: {
id: 'mock_resource_root_node',
label: 'mock resource root node',
iconPath: undefined,
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: 'mock_resource_root_node'
}
};
const mockTokens = {};
mockTokens[mockTenantId] = {
token: 'mock_token',
tokenType: 'Bearer'
};
const mockDatabases: AzureResourceDatabase[] = [
{
name: 'mock database 1',
serverName: 'mock database server 1',
serverFullName: 'mock database server full name 1',
loginName: 'mock login'
},
{
name: 'mock database 2',
serverName: 'mock database server 2',
serverFullName: 'mock database server full name 2',
loginName: 'mock login'
}
];
describe('AzureResourceDatabaseTreeDataProvider.info', function(): void {
beforeEach(() => {
mockDatabaseService = TypeMoq.Mock.ofType<IAzureResourceDatabaseService>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
});
it('Should be correct when created.', async function(): Promise<void> {
const treeDataProvider = new AzureResourceDatabaseTreeDataProvider(mockDatabaseService.object, mockApiWrapper.object, mockExtensionContext.object);
const treeItem = await treeDataProvider.getTreeItem(mockResourceRootNode);
should(treeItem.id).equal(mockResourceRootNode.treeItem.id);
should(treeItem.label).equal(mockResourceRootNode.treeItem.label);
should(treeItem.collapsibleState).equal(mockResourceRootNode.treeItem.collapsibleState);
should(treeItem.contextValue).equal(mockResourceRootNode.treeItem.contextValue);
});
});
describe('AzureResourceDatabaseTreeDataProvider.getChildren', function(): void {
beforeEach(() => {
mockDatabaseService = TypeMoq.Mock.ofType<IAzureResourceDatabaseService>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens));
mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabases));
mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString());
});
it('Should return container node when element is undefined.', async function(): Promise<void> {
const treeDataProvider = new AzureResourceDatabaseTreeDataProvider(mockDatabaseService.object, mockApiWrapper.object, mockExtensionContext.object);
const children = await treeDataProvider.getChildren();
should(children).Array();
should(children.length).equal(1);
const child = children[0];
should(child.account).undefined();
should(child.subscription).undefined();
should(child.tenantId).undefined();
should(child.treeItem.id).equal('azure.resource.providers.database.treeDataProvider.databaseContainer');
should(child.treeItem.label).equal('SQL Databases');
should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
should(child.treeItem.contextValue).equal('azure.resource.itemType.databaseContainer');
});
it('Should return resource nodes when it is container node.', async function(): Promise<void> {
const treeDataProvider = new AzureResourceDatabaseTreeDataProvider(mockDatabaseService.object, mockApiWrapper.object, mockExtensionContext.object);
const children = await treeDataProvider.getChildren(mockResourceRootNode);
should(children).Array();
should(children.length).equal(mockDatabases.length);
for (let ix = 0; ix < children.length; ix++) {
const child = children[ix];
const database = mockDatabases[ix];
should(child.account).equal(mockAccount);
should(child.subscription).equal(mockSubscription);
should(child.tenantId).equal(mockTenantId);
should(child.treeItem.id).equal(`databaseServer_${database.serverFullName}.database_${database.name}`);
should(child.treeItem.label).equal(`${database.name} (${database.serverName})`);
should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None);
should(child.treeItem.contextValue).equal(AzureResourceItemType.database);
}
});
});

View File

@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import 'mocha';
import { azureResource } from '../../../../azureResource/azure-resource';
import { ApiWrapper } from '../../../../apiWrapper';
import { IAzureResourceDatabaseServerService } from '../../../../azureResource/providers/databaseServer/interfaces';
import { AzureResourceDatabaseServerTreeDataProvider } from '../../../../azureResource/providers/databaseServer/databaseServerTreeDataProvider';
import { AzureResourceDatabaseServer } from '../../../../azureResource/providers/databaseServer/models';
import { AzureResourceItemType } from '../../../../azureResource/constants';
// Mock services
let mockDatabaseServerService: TypeMoq.IMock<IAzureResourceDatabaseServerService>;
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
// Mock test data
const mockAccount: sqlops.Account = {
key: {
accountId: 'mock_account',
providerId: 'mock_provider'
},
displayInfo: {
displayName: 'mock_account@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test'
},
properties: undefined,
isStale: false
};
const mockSubscription: azureResource.AzureResourceSubscription = {
id: 'mock_subscription',
name: 'mock subscription'
};
const mockTenantId: string = 'mock_tenant';
const mockResourceRootNode: azureResource.IAzureResourceNode = {
account: mockAccount,
subscription: mockSubscription,
tenantId: mockTenantId,
treeItem: {
id: 'mock_resource_root_node',
label: 'mock resource root node',
iconPath: undefined,
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: 'mock_resource_root_node'
}
};
const mockTokens = {};
mockTokens[mockTenantId] = {
token: 'mock_token',
tokenType: 'Bearer'
};
const mockDatabaseServers: AzureResourceDatabaseServer[] = [
{
name: 'mock database server 1',
fullName: 'mock database server full name 1',
loginName: 'mock login',
defaultDatabaseName: 'master'
},
{
name: 'mock database server 2',
fullName: 'mock database server full name 2',
loginName: 'mock login',
defaultDatabaseName: 'master'
}
];
describe('AzureResourceDatabaseServerTreeDataProvider.info', function(): void {
beforeEach(() => {
mockDatabaseServerService = TypeMoq.Mock.ofType<IAzureResourceDatabaseServerService>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
});
it('Should be correct when created.', async function(): Promise<void> {
const treeDataProvider = new AzureResourceDatabaseServerTreeDataProvider(mockDatabaseServerService.object, mockApiWrapper.object, mockExtensionContext.object);
const treeItem = await treeDataProvider.getTreeItem(mockResourceRootNode);
should(treeItem.id).equal(mockResourceRootNode.treeItem.id);
should(treeItem.label).equal(mockResourceRootNode.treeItem.label);
should(treeItem.collapsibleState).equal(mockResourceRootNode.treeItem.collapsibleState);
should(treeItem.contextValue).equal(mockResourceRootNode.treeItem.contextValue);
});
});
describe('AzureResourceDatabaseServerTreeDataProvider.getChildren', function(): void {
beforeEach(() => {
mockDatabaseServerService = TypeMoq.Mock.ofType<IAzureResourceDatabaseServerService>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens));
mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockDatabaseServers));
mockExtensionContext.setup((o) => o.asAbsolutePath(TypeMoq.It.isAnyString())).returns(() => TypeMoq.It.isAnyString());
});
it('Should return container node when element is undefined.', async function(): Promise<void> {
const treeDataProvider = new AzureResourceDatabaseServerTreeDataProvider(mockDatabaseServerService.object, mockApiWrapper.object, mockExtensionContext.object);
const children = await treeDataProvider.getChildren();
should(children).Array();
should(children.length).equal(1);
const child = children[0];
should(child.account).undefined();
should(child.subscription).undefined();
should(child.tenantId).undefined();
should(child.treeItem.id).equal('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainer');
should(child.treeItem.label).equal('SQL Servers');
should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
should(child.treeItem.contextValue).equal('azure.resource.itemType.databaseServerContainer');
});
it('Should return resource nodes when it is container node.', async function(): Promise<void> {
const treeDataProvider = new AzureResourceDatabaseServerTreeDataProvider(mockDatabaseServerService.object, mockApiWrapper.object, mockExtensionContext.object);
const children = await treeDataProvider.getChildren(mockResourceRootNode);
should(children).Array();
should(children.length).equal(mockDatabaseServers.length);
for (let ix = 0; ix < children.length; ix++) {
const child = children[ix];
const databaseServer = mockDatabaseServers[ix];
should(child.account).equal(mockAccount);
should(child.subscription).equal(mockSubscription);
should(child.tenantId).equal(mockTenantId);
should(child.treeItem.id).equal(`databaseServer_${databaseServer.name}`);
should(child.treeItem.label).equal(databaseServer.name);
should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None);
should(child.treeItem.contextValue).equal(AzureResourceItemType.databaseServer);
}
});
});

View File

@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import 'mocha';
import { fail } from 'assert';
import { azureResource } from '../../azureResource/azure-resource';
import { AzureResourceService } from '../../azureResource/resourceService';
// Mock test data
const mockAccount: sqlops.Account = {
key: {
accountId: 'mock_account',
providerId: 'mock_provider'
},
displayInfo: {
displayName: 'mock_account@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test'
},
properties: undefined,
isStale: false
};
const mockSubscription: azureResource.AzureResourceSubscription = {
id: 'mock_subscription',
name: 'mock subscription'
};
const mockTenantId: string = 'mock_tenant';
let mockResourceTreeDataProvider1: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
let mockResourceProvider1: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
let mockResourceTreeDataProvider2: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
let mockResourceProvider2: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
const resourceService: AzureResourceService = AzureResourceService.getInstance();
describe('AzureResourceService.listResourceProviderIds', function(): void {
beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1');
mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object);
mockResourceTreeDataProvider2 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider2.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider2.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider2 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider2.setup((o) => o.providerId).returns(() => 'mockResourceProvider2');
mockResourceProvider2.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider2.object);
resourceService.clearResourceProviders();
resourceService.areResourceProvidersLoaded = true;
});
it('Should be correct when registering providers.', async function(): Promise<void> {
resourceService.registerResourceProvider(mockResourceProvider1.object);
let providerIds = await resourceService.listResourceProviderIds();
should(providerIds).Array();
should(providerIds.length).equal(1);
should(providerIds[0]).equal(mockResourceProvider1.object.providerId);
resourceService.registerResourceProvider(mockResourceProvider2.object);
providerIds = await resourceService.listResourceProviderIds();
should(providerIds).Array();
should(providerIds.length).equal(2);
should(providerIds[0]).equal(mockResourceProvider1.object.providerId);
should(providerIds[1]).equal(mockResourceProvider2.object.providerId);
});
});
describe('AzureResourceService.getRootChildren', function(): void {
beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1');
mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object);
resourceService.clearResourceProviders();
resourceService.registerResourceProvider(mockResourceProvider1.object);
resourceService.areResourceProvidersLoaded = true;
});
it('Should be correct when provider id is correct.', async function(): Promise<void> {
const children = await resourceService.getRootChildren(mockResourceProvider1.object.providerId, mockAccount, mockSubscription, mockTenantId);
should(children).Array();
});
it('Should throw exceptions when provider id is incorrect.', async function(): Promise<void> {
const providerId = 'non_existent_provider_id';
try {
await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId);
} catch (error) {
should(error.message).equal(`Azure resource provider doesn't exist. Id: ${providerId}`);
return;
}
fail();
});
});
describe('AzureResourceService.getChildren', function(): void {
beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getChildren(TypeMoq.It.isAny())).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1');
mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object);
resourceService.clearResourceProviders();
resourceService.registerResourceProvider(mockResourceProvider1.object);
resourceService.areResourceProvidersLoaded = true;
});
it('Should be correct when provider id is correct.', async function(): Promise<void> {
const children = await resourceService.getChildren(mockResourceProvider1.object.providerId, TypeMoq.It.isAny());
should(children).Array();
});
it('Should throw exceptions when provider id is incorrect.', async function(): Promise<void> {
const providerId = 'non_existent_provider_id';
try {
await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId);
} catch (error) {
should(error.message).equal(`Azure resource provider doesn't exist. Id: ${providerId}`);
return;
}
fail();
});
});
describe('AzureResourceService.getTreeItem', function(): void {
beforeEach(() => {
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getChildren(TypeMoq.It.isAny())).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1');
mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object);
resourceService.clearResourceProviders();
resourceService.registerResourceProvider(mockResourceProvider1.object);
resourceService.areResourceProvidersLoaded = true;
});
it('Should be correct when provider id is correct.', async function(): Promise<void> {
const treeItem = await resourceService.getTreeItem(mockResourceProvider1.object.providerId, TypeMoq.It.isAny());
should(treeItem).Object();
});
it('Should throw exceptions when provider id is incorrect.', async function(): Promise<void> {
const providerId = 'non_existent_provider_id';
try {
await resourceService.getRootChildren(providerId, mockAccount, mockSubscription, mockTenantId);
} catch (error) {
should(error.message).equal(`Azure resource provider doesn't exist. Id: ${providerId}`);
return;
}
fail();
});
});

View File

@@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import 'mocha';
import { azureResource } from '../../azureResource/azure-resource';
import { AzureResourceService } from '../../azureResource/resourceService';
import { AzureResourceResourceTreeNode } from '../../azureResource/resourceTreeNode';
const resourceService = AzureResourceService.getInstance();
// Mock test data
const mockAccount: sqlops.Account = {
key: {
accountId: 'mock_account',
providerId: 'mock_provider'
},
displayInfo: {
displayName: 'mock_account@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test'
},
properties: undefined,
isStale: false
};
const mockSubscription: azureResource.AzureResourceSubscription = {
id: 'mock_subscription',
name: 'mock subscription'
};
const mockTenantId: string = 'mock_tenant';
const mockResourceProviderId: string = 'mock_resource_provider';
const mockResourceRootNode: azureResource.IAzureResourceNode = {
account: mockAccount,
subscription: mockSubscription,
tenantId: mockTenantId,
treeItem: {
id: 'mock_resource_root_node',
label: 'mock resource root node',
iconPath: undefined,
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: 'mock_resource_root_node'
}
};
const mockResourceNode1: azureResource.IAzureResourceNode = {
account: mockAccount,
subscription: mockSubscription,
tenantId: mockTenantId,
treeItem: {
id: 'mock_resource_node_1',
label: 'mock resource node 1',
iconPath: undefined,
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextValue: 'mock_resource_node'
}
};
const mockResourceNode2: azureResource.IAzureResourceNode = {
account: mockAccount,
subscription: mockSubscription,
tenantId: mockTenantId,
treeItem: {
id: 'mock_resource_node_2',
label: 'mock resource node 2',
iconPath: undefined,
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextValue: 'mock_resource_node'
}
};
const mockResourceNodes: azureResource.IAzureResourceNode[] = [mockResourceNode1, mockResourceNode2];
let mockResourceTreeDataProvider: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
let mockResourceProvider: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
describe('AzureResourceResourceTreeNode.info', function(): void {
beforeEach(() => {
mockResourceTreeDataProvider = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider.setup((o) => o.getTreeItem(mockResourceRootNode)).returns(() => mockResourceRootNode.treeItem);
mockResourceTreeDataProvider.setup((o) => o.getChildren(mockResourceRootNode)).returns(() => Promise.resolve(mockResourceNodes));
mockResourceProvider = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider.setup((o) => o.providerId).returns(() => mockResourceProviderId);
mockResourceProvider.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider.object);
resourceService.clearResourceProviders();
resourceService.registerResourceProvider(mockResourceProvider.object);
resourceService.areResourceProvidersLoaded = true;
});
it('Should be correct when created.', async function(): Promise<void> {
const resourceTreeNode = new AzureResourceResourceTreeNode({
resourceProviderId: mockResourceProviderId,
resourceNode: mockResourceRootNode
}, undefined);
should(resourceTreeNode.nodePathValue).equal(mockResourceRootNode.treeItem.id);
const treeItem = await resourceTreeNode.getTreeItem();
should(treeItem.id).equal(mockResourceRootNode.treeItem.id);
should(treeItem.label).equal(mockResourceRootNode.treeItem.label);
should(treeItem.collapsibleState).equal(mockResourceRootNode.treeItem.collapsibleState);
should(treeItem.contextValue).equal(mockResourceRootNode.treeItem.contextValue);
const nodeInfo = resourceTreeNode.getNodeInfo();
should(nodeInfo.label).equal(mockResourceRootNode.treeItem.label);
should(nodeInfo.isLeaf).equal(mockResourceRootNode.treeItem.collapsibleState === vscode.TreeItemCollapsibleState.None);
should(nodeInfo.nodeType).equal(mockResourceRootNode.treeItem.contextValue);
should(nodeInfo.iconType).equal(mockResourceRootNode.treeItem.contextValue);
});
});
describe('AzureResourceResourceTreeNode.getChildren', function(): void {
beforeEach(() => {
mockResourceTreeDataProvider = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider.setup((o) => o.getChildren(mockResourceRootNode)).returns(() => Promise.resolve(mockResourceNodes));
mockResourceProvider = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider.setup((o) => o.providerId).returns(() => mockResourceProviderId);
mockResourceProvider.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider.object);
resourceService.clearResourceProviders();
resourceService.registerResourceProvider(mockResourceProvider.object);
resourceService.areResourceProvidersLoaded = true;
});
it('Should return resource nodes when it is container node.', async function(): Promise<void> {
const resourceTreeNode = new AzureResourceResourceTreeNode({
resourceProviderId: mockResourceProviderId,
resourceNode: mockResourceRootNode
}, undefined);
const children = await resourceTreeNode.getChildren();
mockResourceTreeDataProvider.verify((o) => o.getChildren(mockResourceRootNode), TypeMoq.Times.once());
should(children).Array();
should(children.length).equal(mockResourceNodes.length);
for (let ix = 0; ix < children.length; ix++) {
const child = children[ix];
should(child).instanceOf(AzureResourceResourceTreeNode);
const childNode = (child as AzureResourceResourceTreeNode).resourceNodeWithProviderId;
should(childNode.resourceProviderId).equal(mockResourceProviderId);
should(childNode.resourceNode.account).equal(mockAccount);
should(childNode.resourceNode.subscription).equal(mockSubscription);
should(childNode.resourceNode.tenantId).equal(mockTenantId);
should(childNode.resourceNode.treeItem.id).equal(mockResourceNodes[ix].treeItem.id);
should(childNode.resourceNode.treeItem.label).equal(mockResourceNodes[ix].treeItem.label);
should(childNode.resourceNode.treeItem.collapsibleState).equal(mockResourceNodes[ix].treeItem.collapsibleState);
should(childNode.resourceNode.treeItem.contextValue).equal(mockResourceNodes[ix].treeItem.contextValue);
}
});
it('Should return empty when it is leaf node.', async function(): Promise<void> {
const resourceTreeNode = new AzureResourceResourceTreeNode({
resourceProviderId: mockResourceProviderId,
resourceNode: mockResourceNode1
}, undefined);
const children = await resourceTreeNode.getChildren();
mockResourceTreeDataProvider.verify((o) => o.getChildren(), TypeMoq.Times.exactly(0));
should(children).Array();
should(children.length).equal(0);
});
});

View File

@@ -26,7 +26,7 @@ describe('AzureResourceAccountNotSignedInTreeNode.info', function(): void {
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None);
should(treeItem.command).not.undefined();
should(treeItem.command.title).equal(label);
should(treeItem.command.command).equal('azureresource.signin');
should(treeItem.command.command).equal('azure.resource.signin');
const nodeInfo = treeNode.getNodeInfo();
should(nodeInfo.isLeaf).true();

View File

@@ -10,35 +10,38 @@ import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import 'mocha';
import { ServiceClientCredentials } from 'ms-rest';
import { TokenCredentials } from 'ms-rest';
import { AppContext } from '../../../appContext';
import { AzureResourceServicePool } from '../../../azureResource/servicePool';
import { azureResource } from '../../../azureResource/azure-resource';
import {
IAzureResourceCacheService,
IAzureResourceContextService,
IAzureResourceCredentialService,
IAzureResourceSubscriptionService,
IAzureResourceSubscriptionFilterService
IAzureResourceSubscriptionFilterService,
IAzureResourceTenantService
} from '../../../azureResource/interfaces';
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode';
import { AzureResourceSubscription } from '../../../azureResource/models';
import { AzureResourceSubscriptionTreeNode } from '../../../azureResource/tree/subscriptionTreeNode';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode';
import { AzureResourceItemType, AzureResourceServiceNames } from '../../../azureResource/constants';
import { AzureResourceMessageTreeNode } from '../../../azureResource/messageTreeNode';
import { ApiWrapper } from '../../../apiWrapper';
import { generateGuid } from '../../../azureResource/utils';
// Mock services
const mockServicePool = AzureResourceServicePool.getInstance();
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
let mockContextService: TypeMoq.IMock<IAzureResourceContextService>;
let mockCredentialService: TypeMoq.IMock<IAzureResourceCredentialService>;
let mockSubscriptionService: TypeMoq.IMock<IAzureResourceSubscriptionService>;
let mockSubscriptionFilterService: TypeMoq.IMock<IAzureResourceSubscriptionFilterService>;
let mockTenantService: TypeMoq.IMock<IAzureResourceTenantService>;
let mockAppContext: AppContext;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data
const mockTenantId = 'mock_tenant_id';
const mockAccount: sqlops.Account = {
key: {
accountId: 'mock_account',
@@ -49,51 +52,68 @@ const mockAccount: sqlops.Account = {
accountType: 'Microsoft',
contextualDisplayName: 'test'
},
properties: undefined,
properties: {
tenants: [
{
id: mockTenantId
}
]
},
isStale: false
};
const mockCredential = TypeMoq.Mock.ofType<ServiceClientCredentials>().object;
const mockCredentials = [mockCredential];
const mockSubscription1: AzureResourceSubscription = {
const mockSubscription1: azureResource.AzureResourceSubscription = {
id: 'mock_subscription_1',
name: 'mock subscription 1'
};
const mockSubscription2: AzureResourceSubscription = {
const mockSubscription2: azureResource.AzureResourceSubscription = {
id: 'mock_subscription_2',
name: 'mock subscription 2'
};
const mockSubscriptions = [mockSubscription1, mockSubscription2];
const mockFilteredSubscriptions = [mockSubscription1];
let mockSubscriptionCache: { subscriptions: { [accountId: string]: AzureResourceSubscription[]} };
const mockTokens = {};
mockTokens[mockTenantId] = {
token: 'mock_token',
tokenType: 'Bearer'
};
const mockCredential = new TokenCredentials(mockTokens[mockTenantId].token, mockTokens[mockTenantId].tokenType);
let mockSubscriptionCache: azureResource.AzureResourceSubscription[] = [];
describe('AzureResourceAccountTreeNode.info', function(): void {
beforeEach(() => {
mockContextService = TypeMoq.Mock.ofType<IAzureResourceContextService>();
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockCredentialService = TypeMoq.Mock.ofType<IAzureResourceCredentialService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
mockTenantService = TypeMoq.Mock.ofType<IAzureResourceTenantService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = { subscriptions: {} };
mockSubscriptionCache = [];
mockServicePool.contextService = mockContextService.object;
mockServicePool.cacheService = mockCacheService.object;
mockServicePool.credentialService = mockCredentialService.object;
mockServicePool.subscriptionService = mockSubscriptionService.object;
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, mockTenantService.object);
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens));
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
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);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId));
});
it('Should be correct when created.', async function(): Promise<void> {
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId})`;
@@ -114,14 +134,17 @@ describe('AzureResourceAccountTreeNode.info', function(): void {
});
it('Should be correct when there are subscriptions listed.', async function(): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined));
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId}) (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
await accountTreeNode.getChildren();
const subscriptionNodes = await accountTreeNode.getChildren();
should(subscriptionNodes).Array();
should(subscriptionNodes.length).equal(mockSubscriptions.length);
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.label).equal(accountTreeNodeLabel);
@@ -131,14 +154,17 @@ describe('AzureResourceAccountTreeNode.info', function(): void {
});
it('Should be correct when there are subscriptions filtered.', async function(): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId}) (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
await accountTreeNode.getChildren();
const subscriptionNodes = await accountTreeNode.getChildren();
should(subscriptionNodes).Array();
should(subscriptionNodes.length).equal(mockFilteredSubscriptions.length);
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.label).equal(accountTreeNodeLabel);
@@ -150,36 +176,41 @@ describe('AzureResourceAccountTreeNode.info', function(): void {
describe('AzureResourceAccountTreeNode.getChildren', function(): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockCredentialService = TypeMoq.Mock.ofType<IAzureResourceCredentialService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
mockTenantService = TypeMoq.Mock.ofType<IAzureResourceTenantService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = { subscriptions: {} };
mockSubscriptionCache = [];
mockServicePool.cacheService = mockCacheService.object;
mockServicePool.credentialService = mockCredentialService.object;
mockServicePool.subscriptionService = mockSubscriptionService.object;
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, mockTenantService.object);
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens));
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
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);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId));
});
it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function(): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined));
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren();
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());
mockApiWrapper.verify((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0));
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
@@ -192,43 +223,42 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
should(children).Array();
should(children.length).equal(mockSubscriptions.length);
should(Object.keys(mockSubscriptionCache.subscriptions)).deepEqual([mockAccount.key.accountId]);
should(mockSubscriptionCache.subscriptions[mockAccount.key.accountId]).deepEqual(mockSubscriptions);
should(mockSubscriptionCache).deepEqual(mockSubscriptions);
for (let ix = 0; ix < mockSubscriptions.length; ix++) {
const child = children[ix];
const subscription = mockSubscriptions[ix];
should(child).instanceof(AzureResourceSubscriptionTreeNode);
should(child.nodePathValue).equal(`subscription_${subscription.id}`);
should(child.nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${subscription.id}.tenant_${mockTenantId}`);
}
});
it('Should load subscriptions from cache when it is not clearing cache.', async function(): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
await accountTreeNode.getChildren();
const children = await accountTreeNode.getChildren();
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));
mockApiWrapper.verify((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), 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());
should(children.length).equal(mockSubscriptionCache.subscriptions[mockAccount.key.accountId].length);
should(children.length).equal(mockSubscriptionCache.length);
for (let ix = 0; ix < mockSubscriptionCache.subscriptions[mockAccount.key.accountId].length; ix++) {
should(children[ix].nodePathValue).equal(`subscription_${mockSubscriptionCache.subscriptions[mockAccount.key.accountId][ix].id}`);
for (let ix = 0; ix < mockSubscriptionCache.length; ix++) {
should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockSubscriptionCache[ix].id}.tenant_${mockTenantId}`);
}
});
it('Should handle when there is no subscriptions.', async function(): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(undefined));
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(undefined));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren();
@@ -242,10 +272,10 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
});
it('Should honor subscription filtering.', async function(): Promise<void> {
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren();
@@ -255,23 +285,25 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
should(children.length).equal(mockFilteredSubscriptions.length);
for (let ix = 0; ix < mockFilteredSubscriptions.length; ix++) {
should(children[ix].nodePathValue).equal(`subscription_${mockFilteredSubscriptions[ix].id}`);
should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockFilteredSubscriptions[ix].id}.tenant_${mockTenantId}`);
}
});
it('Should handle errors.', async function(): Promise<void> {
const mockError = 'Test error';
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => { throw new Error(mockError); });
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredential)).returns(() => Promise.resolve(mockSubscriptions));
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const mockError = 'Test error';
mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); });
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren();
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once());
mockApiWrapper.verify((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredential), TypeMoq.Times.once());
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), 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());
mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.never());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
should(children).Array();
should(children.length).equal(1);
@@ -283,12 +315,33 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
describe('AzureResourceAccountTreeNode.clearCache', function() : void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
mockTenantService = TypeMoq.Mock.ofType<IAzureResourceTenantService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockSubscriptionCache = [];
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
mockAppContext.registerService<IAzureResourceTenantService>(AzureResourceServiceNames.tenantService, mockTenantService.object);
mockApiWrapper.setup((o) => o.getSecurityToken(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockTokens));
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache = mockSubscriptions);
mockTenantService.setup((o) => o.getTenantId(TypeMoq.It.isAny())).returns(() => Promise.resolve(mockTenantId));
});
it('Should clear cache.', async function(): Promise<void> {
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object);
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
accountTreeNode.clearCache();
should(accountTreeNode.isClearingCache).true();
});
});
});

View File

@@ -1,219 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import 'mocha';
import { ServiceClientCredentials } from 'ms-rest';
import { AzureResourceServicePool } from '../../../azureResource/servicePool';
import {
IAzureResourceCacheService,
IAzureResourceContextService,
IAzureResourceCredentialService,
IAzureResourceDatabaseService
} from '../../../azureResource/interfaces';
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
import { AzureResourceSubscription, AzureResourceDatabase } from '../../../azureResource/models';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode';
import { AzureResourceDatabaseContainerTreeNode } from '../../../azureResource/tree/databaseContainerTreeNode';
import { AzureResourceDatabaseTreeNode } from '../../../azureResource/tree/databaseTreeNode';
// Mock services
const mockServicePool = AzureResourceServicePool.getInstance();
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
let mockContextService: TypeMoq.IMock<IAzureResourceContextService>;
let mockCredentialService: TypeMoq.IMock<IAzureResourceCredentialService>;
let mockDatabaseService: TypeMoq.IMock<IAzureResourceDatabaseService>;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data
const mockAccount: sqlops.Account = {
key: {
accountId: 'mock_account',
providerId: 'mock_provider'
},
displayInfo: {
displayName: 'mock_account@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test'
},
properties: undefined,
isStale: false
};
const mockCredential = TypeMoq.Mock.ofType<ServiceClientCredentials>().object;
const mockCredentials = [mockCredential];
const mockSubscription: AzureResourceSubscription = {
id: 'mock_subscription',
name: 'mock subscription'
};
const mockDatabase1: AzureResourceDatabase = {
name: 'mock database 1',
serverName: 'mock server 1',
serverFullName: 'mock server 1',
loginName: 'mock user 1'
};
const mockDatabase2: AzureResourceDatabase = {
name: 'mock database 2',
serverName: 'mock server 2',
serverFullName: 'mock server 2',
loginName: 'mock user 2'
};
const mockDatabases = [mockDatabase1, mockDatabase2];
let mockDatabaseContainerCache: { databases: { [subscriptionId: string]: AzureResourceDatabase[] } };
describe('AzureResourceDatabaseContainerTreeNode.info', function(): void {
beforeEach(() => {
mockContextService = TypeMoq.Mock.ofType<IAzureResourceContextService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockServicePool.contextService = mockContextService.object;
});
it('Should be correct when created.', async function(): Promise<void> {
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const databaseContainerTreeNodeLabel = 'SQL Databases';
should(databaseContainerTreeNode.nodePathValue).equal('databaseContainer');
const treeItem = await databaseContainerTreeNode.getTreeItem();
should(treeItem.label).equal(databaseContainerTreeNodeLabel);
should(treeItem.contextValue).equal(AzureResourceItemType.databaseContainer);
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
const nodeInfo = databaseContainerTreeNode.getNodeInfo();
should(nodeInfo.isLeaf).false();
should(nodeInfo.label).equal(databaseContainerTreeNodeLabel);
should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseContainer);
should(nodeInfo.iconType).equal(AzureResourceItemType.databaseContainer);
});
});
describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void {
beforeEach(() => {
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockCredentialService = TypeMoq.Mock.ofType<IAzureResourceCredentialService>();
mockDatabaseService = TypeMoq.Mock.ofType<IAzureResourceDatabaseService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockDatabaseContainerCache = { databases: {} };
mockServicePool.cacheService = mockCacheService.object;
mockServicePool.credentialService = mockCredentialService.object;
mockServicePool.databaseService = mockDatabaseService.object;
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);
});
it('Should load databases from scratch and update cache when it is clearing cache.', async function(): Promise<void> {
mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabases));
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseContainerTreeNode.getChildren();
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());
should(databaseContainerTreeNode.isClearingCache).false();
should(children).Array();
should(children.length).equal(mockDatabases.length);
should(Object.keys(mockDatabaseContainerCache.databases)).deepEqual([mockSubscription.id]);
should(mockDatabaseContainerCache.databases[mockSubscription.id]).deepEqual(mockDatabases);
for (let ix = 0; ix < mockDatabases.length; ix++) {
const child = children[ix];
const database = mockDatabases[ix];
should(child).instanceof(AzureResourceDatabaseTreeNode);
should(child.nodePathValue).equal(`database_${database.name}`);
}
});
it('Should load databases from cache when it is not clearing cache.', async function(): Promise<void> {
mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabases));
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
await databaseContainerTreeNode.getChildren();
const children = await databaseContainerTreeNode.getChildren();
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));
should(children.length).equal(mockDatabaseContainerCache.databases[mockSubscription.id].length);
for (let ix = 0; ix < mockDatabaseContainerCache.databases[mockSubscription.id].length; ix++) {
should(children[ix].nodePathValue).equal(`database_${mockDatabaseContainerCache.databases[mockSubscription.id][ix].name}`);
}
});
it('Should handle when there is no databases.', async function(): Promise<void> {
mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(undefined));
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseContainerTreeNode.getChildren();
should(children).Array();
should(children.length).equal(1);
should(children[0]).instanceof(AzureResourceMessageTreeNode);
should(children[0].nodePathValue).startWith('message_');
should(children[0].getNodeInfo().label).equal('No SQL Databases found.');
});
it('Should handle errors.', async function(): Promise<void> {
const mockError = 'Test error';
mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => { throw new Error(mockError); });
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseContainerTreeNode.getChildren();
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());
should(children).Array();
should(children.length).equal(1);
should(children[0]).instanceof(AzureResourceMessageTreeNode);
should(children[0].nodePathValue).startWith('message_');
should(children[0].getNodeInfo().label).equal(`Error: ${mockError}`);
});
});
describe('AzureResourceDatabaseContainerTreeNode.clearCache', function() : void {
beforeEach(() => {
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
});
it('Should clear cache.', async function(): Promise<void> {
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
databaseContainerTreeNode.clearCache();
should(databaseContainerTreeNode.isClearingCache).true();
});
});

View File

@@ -1,219 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import 'mocha';
import { ServiceClientCredentials } from 'ms-rest';
import { AzureResourceServicePool } from '../../../azureResource/servicePool';
import {
IAzureResourceCacheService,
IAzureResourceContextService,
IAzureResourceCredentialService,
IAzureResourceDatabaseServerService
} from '../../../azureResource/interfaces';
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../../../azureResource/models';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode';
import { AzureResourceDatabaseServerContainerTreeNode } from '../../../azureResource/tree/databaseServerContainerTreeNode';
import { AzureResourceDatabaseServerTreeNode } from '../../../azureResource/tree/databaseServerTreeNode';
// Mock services
const mockServicePool = AzureResourceServicePool.getInstance();
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
let mockContextService: TypeMoq.IMock<IAzureResourceContextService>;
let mockCredentialService: TypeMoq.IMock<IAzureResourceCredentialService>;
let mockDatabaseServerService: TypeMoq.IMock<IAzureResourceDatabaseServerService>;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data
const mockAccount: sqlops.Account = {
key: {
accountId: 'mock_account',
providerId: 'mock_provider'
},
displayInfo: {
displayName: 'mock_account@test.com',
accountType: 'Microsoft',
contextualDisplayName: 'test'
},
properties: undefined,
isStale: false
};
const mockCredential = TypeMoq.Mock.ofType<ServiceClientCredentials>().object;
const mockCredentials = [mockCredential];
const mockSubscription: AzureResourceSubscription = {
id: 'mock_subscription',
name: 'mock subscription'
};
const mockDatabaseServer1: AzureResourceDatabaseServer = {
name: 'mock server 1',
fullName: 'mock server 1',
loginName: 'mock user 1',
defaultDatabaseName: 'master'
};
const mockDatabaseServer2: AzureResourceDatabaseServer = {
name: 'mock server 2',
fullName: 'mock server 2',
loginName: 'mock user 2',
defaultDatabaseName: 'master'
};
const mockDatabaseServers = [mockDatabaseServer1, mockDatabaseServer2];
let mockDatabaseServerContainerCache: { databaseServers: { [subscriptionId: string]: AzureResourceDatabaseServer[] } };
describe('AzureResourceDatabaseServerContainerTreeNode.info', function(): void {
beforeEach(() => {
mockContextService = TypeMoq.Mock.ofType<IAzureResourceContextService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockServicePool.contextService = mockContextService.object;
});
it('Should be correct when created.', async function(): Promise<void> {
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const databaseServerContainerTreeNodeLabel = 'SQL Servers';
should(databaseServerContainerTreeNode.nodePathValue).equal('databaseServerContainer');
const treeItem = await databaseServerContainerTreeNode.getTreeItem();
should(treeItem.label).equal(databaseServerContainerTreeNodeLabel);
should(treeItem.contextValue).equal(AzureResourceItemType.databaseServerContainer);
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
const nodeInfo = databaseServerContainerTreeNode.getNodeInfo();
should(nodeInfo.isLeaf).false();
should(nodeInfo.label).equal(databaseServerContainerTreeNodeLabel);
should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseServerContainer);
should(nodeInfo.iconType).equal(AzureResourceItemType.databaseServerContainer);
});
});
describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function(): void {
beforeEach(() => {
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockCredentialService = TypeMoq.Mock.ofType<IAzureResourceCredentialService>();
mockDatabaseServerService = TypeMoq.Mock.ofType<IAzureResourceDatabaseServerService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockDatabaseServerContainerCache = { databaseServers: {} };
mockServicePool.cacheService = mockCacheService.object;
mockServicePool.credentialService = mockCredentialService.object;
mockServicePool.databaseServerService = mockDatabaseServerService.object;
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);
});
it('Should load database servers from scratch and update cache when it is clearing cache.', async function(): Promise<void> {
mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabaseServers));
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseServerContainerTreeNode.getChildren();
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());
should(databaseServerContainerTreeNode.isClearingCache).false();
should(children).Array();
should(children.length).equal(mockDatabaseServers.length);
should(Object.keys(mockDatabaseServerContainerCache.databaseServers)).deepEqual([mockSubscription.id]);
should(mockDatabaseServerContainerCache.databaseServers[mockSubscription.id]).deepEqual(mockDatabaseServers);
for (let ix = 0; ix < mockDatabaseServers.length; ix++) {
const child = children[ix];
const databaseServer = mockDatabaseServers[ix];
should(child).instanceof(AzureResourceDatabaseServerTreeNode);
should(child.nodePathValue).equal(`databaseServer_${databaseServer.name}`);
}
});
it('Should load database servers from cache when it is not clearing cache.', async function(): Promise<void> {
mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabaseServers));
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
await databaseServerContainerTreeNode.getChildren();
const children = await databaseServerContainerTreeNode.getChildren();
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));
should(children.length).equal(mockDatabaseServerContainerCache.databaseServers[mockSubscription.id].length);
for (let ix = 0; ix < mockDatabaseServerContainerCache.databaseServers[mockSubscription.id].length; ix++) {
should(children[ix].nodePathValue).equal(`databaseServer_${mockDatabaseServerContainerCache.databaseServers[mockSubscription.id][ix].name}`);
}
});
it('Should handle when there is no database servers.', async function(): Promise<void> {
mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(undefined));
const databaseContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseContainerTreeNode.getChildren();
should(children).Array();
should(children.length).equal(1);
should(children[0]).instanceof(AzureResourceMessageTreeNode);
should(children[0].nodePathValue).startWith('message_');
should(children[0].getNodeInfo().label).equal('No SQL Servers found.');
});
it('Should handle errors.', async function(): Promise<void> {
const mockError = 'Test error';
mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => { throw new Error(mockError); });
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const children = await databaseServerContainerTreeNode.getChildren();
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());
should(children).Array();
should(children.length).equal(1);
should(children[0]).instanceof(AzureResourceMessageTreeNode);
should(children[0].nodePathValue).startWith('message_');
should(children[0].getNodeInfo().label).equal(`Error: ${mockError}`);
});
});
describe('AzureResourceDatabaseServerContainerTreeNode.clearCache', function() : void {
beforeEach(() => {
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
});
it('Should clear cache.', async function(): Promise<void> {
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
databaseServerContainerTreeNode.clearCache();
should(databaseServerContainerTreeNode.isClearingCache).true();
});
});

View File

@@ -1,62 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import 'mocha';
import { AzureResourceServicePool } from '../../../azureResource/servicePool';
import { IAzureResourceContextService } from '../../../azureResource/interfaces';
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
import { AzureResourceDatabaseServer } from '../../../azureResource/models';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { AzureResourceDatabaseServerTreeNode } from '../../../azureResource/tree/databaseServerTreeNode';
// Mock services
const mockServicePool = AzureResourceServicePool.getInstance();
let mockContextService: TypeMoq.IMock<IAzureResourceContextService>;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data
const mockDatabaseServer: AzureResourceDatabaseServer = {
name: 'mock database 1',
fullName: 'mock server 1',
loginName: 'mock user 1',
defaultDatabaseName: 'master'
};
describe('AzureResourceDatabaseServerTreeNode.info', function(): void {
beforeEach(() => {
mockContextService = TypeMoq.Mock.ofType<IAzureResourceContextService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockServicePool.contextService = mockContextService.object;
});
it('Should be correct when created.', async function(): Promise<void> {
const databaseServerTreeNode = new AzureResourceDatabaseServerTreeNode(mockDatabaseServer, mockTreeChangeHandler.object, undefined);
const databaseServerTreeNodeLabel = mockDatabaseServer.name;
should(databaseServerTreeNode.nodePathValue).equal(`databaseServer_${mockDatabaseServer.name}`);
const treeItem = await databaseServerTreeNode.getTreeItem();
should(treeItem.label).equal(databaseServerTreeNodeLabel);
should(treeItem.contextValue).equal(AzureResourceItemType.databaseServer);
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None);
const nodeInfo = databaseServerTreeNode.getNodeInfo();
should(nodeInfo.isLeaf).true();
should(nodeInfo.label).equal(databaseServerTreeNodeLabel);
should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseServer);
should(nodeInfo.iconType).equal(AzureResourceItemType.databaseServer);
});
});

View File

@@ -1,62 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import 'mocha';
import { AzureResourceServicePool } from '../../../azureResource/servicePool';
import { IAzureResourceContextService } from '../../../azureResource/interfaces';
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
import { AzureResourceDatabase } from '../../../azureResource/models';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { AzureResourceDatabaseTreeNode } from '../../../azureResource/tree/databaseTreeNode';
// Mock services
const mockServicePool = AzureResourceServicePool.getInstance();
let mockContextService: TypeMoq.IMock<IAzureResourceContextService>;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
// Mock test data
const mockDatabase: AzureResourceDatabase = {
name: 'mock database 1',
serverName: 'mock server 1',
serverFullName: 'mock server 1',
loginName: 'mock user 1'
};
describe('AzureResourceDatabaseTreeNode.info', function(): void {
beforeEach(() => {
mockContextService = TypeMoq.Mock.ofType<IAzureResourceContextService>();
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockServicePool.contextService = mockContextService.object;
});
it('Should be correct.', async function(): Promise<void> {
const databaseTreeNode = new AzureResourceDatabaseTreeNode(mockDatabase, mockTreeChangeHandler.object, undefined);
const databaseTreeNodeLabel = `${mockDatabase.name} (${mockDatabase.serverName})`;
should(databaseTreeNode.nodePathValue).equal(`database_${mockDatabase.name}`);
const treeItem = await databaseTreeNode.getTreeItem();
should(treeItem.label).equal(databaseTreeNodeLabel);
should(treeItem.contextValue).equal(AzureResourceItemType.database);
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None);
const nodeInfo = databaseTreeNode.getNodeInfo();
should(nodeInfo.isLeaf).true();
should(nodeInfo.label).equal(databaseTreeNodeLabel);
should(nodeInfo.nodeType).equal(AzureResourceItemType.database);
should(nodeInfo.iconType).equal(AzureResourceItemType.database);
});
});

View File

@@ -10,20 +10,24 @@ import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import 'mocha';
import { AppContext } from '../../../appContext';
import { ApiWrapper } from '../../../apiWrapper';
import { AzureResourceServicePool } from '../../../azureResource/servicePool';
import { IAzureResourceContextService } from '../../../azureResource/interfaces';
import { azureResource } from '../../../azureResource/azure-resource';
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
import { AzureResourceSubscription } from '../../../azureResource/models';
import { AzureResourceSubscriptionTreeNode } from '../../../azureResource/tree/subscriptionTreeNode';
import { AzureResourceDatabaseContainerTreeNode } from '../../../azureResource/tree/databaseContainerTreeNode';
import { AzureResourceDatabaseServerContainerTreeNode } from '../../../azureResource/tree/databaseServerContainerTreeNode';
import { AzureResourceItemType } from '../../../azureResource/constants';
import { AzureResourceItemType, AzureResourceServiceNames } from '../../../azureResource/constants';
import { AzureResourceService } from '../../../azureResource/resourceService';
import { AzureResourceResourceTreeNode } from '../../../azureResource/resourceTreeNode';
import { IAzureResourceCacheService } from '../../../azureResource/interfaces';
import { generateGuid } from '../../../azureResource/utils';
// Mock services
const mockServicePool = AzureResourceServicePool.getInstance();
let mockAppContext: AppContext;
let mockContextService: TypeMoq.IMock<IAzureResourceContextService>;
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
@@ -42,24 +46,60 @@ const mockAccount: sqlops.Account = {
isStale: false
};
const mockSubscription: AzureResourceSubscription = {
const mockSubscription: azureResource.AzureResourceSubscription = {
id: 'mock_subscription',
name: 'mock subscription'
};
const mockTenantId: string = 'mock_tenant';
let mockResourceTreeDataProvider1: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
let mockResourceProvider1: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
let mockResourceTreeDataProvider2: TypeMoq.IMock<azureResource.IAzureResourceTreeDataProvider>;
let mockResourceProvider2: TypeMoq.IMock<azureResource.IAzureResourceProvider>;
const resourceService: AzureResourceService = AzureResourceService.getInstance();
describe('AzureResourceSubscriptionTreeNode.info', function(): void {
beforeEach(() => {
mockContextService = TypeMoq.Mock.ofType<IAzureResourceContextService>();
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockServicePool.contextService = mockContextService.object;
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1');
mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object);
mockResourceTreeDataProvider2 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider2.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider2.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider2 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider2.setup((o) => o.providerId).returns(() => 'mockResourceProvider2');
mockResourceProvider2.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider2.object);
resourceService.clearResourceProviders();
resourceService.registerResourceProvider(mockResourceProvider1.object);
resourceService.registerResourceProvider(mockResourceProvider2.object);
resourceService.areResourceProvidersLoaded = true;
});
it('Should be correct when created.', async function(): Promise<void> {
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, mockAppContext, mockTreeChangeHandler.object, undefined);
should(subscriptionTreeNode.nodePathValue).equal(`subscription_${mockSubscription.id}`);
should(subscriptionTreeNode.nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockSubscription.id}.tenant_${mockTenantId}`);
const treeItem = await subscriptionTreeNode.getTreeItem();
should(treeItem.label).equal(mockSubscription.name);
@@ -76,16 +116,52 @@ describe('AzureResourceSubscriptionTreeNode.info', function(): void {
describe('AzureResourceSubscriptionTreeNode.getChildren', function(): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider1.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider1.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider1 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider1.setup((o) => o.providerId).returns(() => 'mockResourceProvider1');
mockResourceProvider1.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider1.object);
mockResourceTreeDataProvider2 = TypeMoq.Mock.ofType<azureResource.IAzureResourceTreeDataProvider>();
mockResourceTreeDataProvider2.setup((o) => o.getChildren()).returns(() => Promise.resolve([TypeMoq.Mock.ofType<azureResource.IAzureResourceNode>().object]));
mockResourceTreeDataProvider2.setup((o) => o.getTreeItem(TypeMoq.It.isAny())).returns(() => Promise.resolve(TypeMoq.It.isAny()));
mockResourceProvider2 = TypeMoq.Mock.ofType<azureResource.IAzureResourceProvider>();
mockResourceProvider2.setup((o) => o.providerId).returns(() => 'mockResourceProvider2');
mockResourceProvider2.setup((o) => o.getTreeDataProvider()).returns(() => mockResourceTreeDataProvider2.object);
resourceService.clearResourceProviders();
resourceService.registerResourceProvider(mockResourceProvider1.object);
resourceService.registerResourceProvider(mockResourceProvider2.object);
resourceService.areResourceProvidersLoaded = true;
});
it('Should load database containers.', async function(): Promise<void> {
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
it('Should return resource containers.', async function(): Promise<void> {
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, mockAppContext, mockTreeChangeHandler.object, undefined);
const children = await subscriptionTreeNode.getChildren();
mockResourceTreeDataProvider1.verify((o) => o.getChildren(), TypeMoq.Times.once());
mockResourceTreeDataProvider2.verify((o) => o.getChildren(), TypeMoq.Times.once());
const expectedChildren = await resourceService.listResourceProviderIds();
should(children).Array();
should(children.length).equal(2);
should(children[0]).instanceof(AzureResourceDatabaseContainerTreeNode);
should(children[1]).instanceof(AzureResourceDatabaseServerContainerTreeNode);
should(children.length).equal(expectedChildren.length);
for (const child of children) {
should(child).instanceOf(AzureResourceResourceTreeNode);
}
});
});

View File

@@ -5,21 +5,28 @@
'use strict';
import * as vscode from 'vscode';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as sqlops from 'sqlops';
import 'mocha';
import { AppContext } from '../../../appContext';
import { ApiWrapper } from '../../../apiWrapper';
import { AzureResourceServicePool } from '../../../azureResource/servicePool';
import { IAzureResourceAccountService } from '../../../azureResource/interfaces';
import { IAzureResourceCacheService, IAzureResourceAccountService } from '../../../azureResource/interfaces';
import { AzureResourceTreeProvider } from '../../../azureResource/tree/treeProvider';
import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode';
import { AzureResourceAccountNotSignedInTreeNode } from '../../../azureResource/tree/accountNotSignedInTreeNode';
import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode';
import { AzureResourceMessageTreeNode } from '../../../azureResource/messageTreeNode';
import { AzureResourceServiceNames } from '../../../azureResource/constants';
import { generateGuid } from '../../../azureResource/utils';
// Mock services
const mockServicePool = AzureResourceServicePool.getInstance();
let mockAppContext: AppContext;
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
let mockApiWrapper: TypeMoq.IMock<ApiWrapper>;
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
let mockAccountService: TypeMoq.IMock<IAzureResourceAccountService>;
// Mock test data
@@ -53,15 +60,23 @@ const mockAccounts = [mockAccount1, mockAccount2];
describe('AzureResourceTreeProvider.getChildren', function(): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
mockAccountService = TypeMoq.Mock.ofType<IAzureResourceAccountService>();
mockServicePool.accountService = mockAccountService.object;
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object);
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
mockAppContext.registerService<IAzureResourceAccountService>(AzureResourceServiceNames.accountService, mockAccountService.object);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
});
it('Should load accounts.', async function(): Promise<void> {
mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(mockAccounts));
const treeProvider = new AzureResourceTreeProvider();
const treeProvider = new AzureResourceTreeProvider(mockAppContext);
treeProvider.isSystemInitialized = true;
const children = await treeProvider.getChildren(undefined);
@@ -83,7 +98,7 @@ describe('AzureResourceTreeProvider.getChildren', function(): void {
it('Should handle when there is no accounts.', async function(): Promise<void> {
mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(undefined));
const treeProvider = new AzureResourceTreeProvider();
const treeProvider = new AzureResourceTreeProvider(mockAppContext);
treeProvider.isSystemInitialized = true;
const children = await treeProvider.getChildren(undefined);
@@ -97,7 +112,7 @@ describe('AzureResourceTreeProvider.getChildren', function(): void {
const mockAccountError = 'Test account error';
mockAccountService.setup((o) => o.getAccounts()).returns(() => { throw new Error(mockAccountError); });
const treeProvider = new AzureResourceTreeProvider();
const treeProvider = new AzureResourceTreeProvider(mockAppContext);
treeProvider.isSystemInitialized = true;
const children = await treeProvider.getChildren(undefined);

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,7 @@ export class DeployConfigPage extends DacFxConfigPage {
}
async start(): Promise<boolean> {
let serverComponent = await this.createServerDropdown(true);
let serverComponent = await this.createServerDropdown(true);
let fileBrowserComponent = await this.createFileBrowser();
this.databaseComponent = await this.createDatabaseTextBox();
this.databaseComponent.title = localize('dacFx.databaseNameTextBox', 'Database Name');
@@ -131,7 +131,7 @@ export class DeployConfigPage extends DacFxConfigPage {
this.model.database = this.databaseTextBox.value;
});
// Initialize with upgrade existing true
//Initialize with upgrade existing true
upgradeRadioButton.checked = true;
this.model.upgradeExisting = true;
@@ -149,10 +149,10 @@ export class DeployConfigPage extends DacFxConfigPage {
}
protected async createDeployDatabaseDropdown(): Promise<sqlops.FormComponent> {
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
required: true
}).component();
// Handle database changes
//Handle database changes
this.databaseDropdown.onValueChanged(async () => {
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
});
@@ -172,7 +172,8 @@ export class DeployConfigPage extends DacFxConfigPage {
}
let values = await this.getDatabaseValues();
if (this.model.database === undefined) {
//set the database to the first dropdown value if upgrading, otherwise it should get set to the textbox value
if (this.model.upgradeExisting) {
this.model.database = values[0].name;
}

View File

@@ -23,7 +23,8 @@
"smoketest": "cd test/smoke && node test/index.js",
"monaco-compile-check": "tsc -p src/tsconfig.monaco.json --noEmit",
"download-builtin-extensions": "node build/lib/builtInExtensions.js",
"check-monaco-editor-compilation": "tsc -p src/tsconfig.monaco.json --noEmit"
"check-monaco-editor-compilation": "tsc -p src/tsconfig.monaco.json --noEmit",
"tslint": "node node_modules/tslint/bin/tslint -c tslint-gci.json -p src/tsconfig.json"
},
"dependencies": {
"@angular/animations": "~4.1.3",
@@ -154,6 +155,7 @@
"source-map": "^0.4.4",
"temp-write": "^3.4.0",
"tslint": "^5.9.1",
"tslint-microsoft-contrib": "^6.0.0",
"typemoq": "^0.3.2",
"typescript": "2.9.2",
"typescript-formatter": "7.1.0",

View File

@@ -42,7 +42,8 @@
"Microsoft.server-report",
"Microsoft.sql-vnext",
"Microsoft.whoisactive",
"Redgate.sql-search"
"Redgate.sql-search",
"IDERA.sqldm-performance-insights"
],
"extensionsGallery": {
"serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json"

View File

@@ -10,16 +10,16 @@ set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5%
:: call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR%
call .\scripts\code.bat %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR%
call .\scripts\code.bat %~dp0\..\extensions\markdown-language-features\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR%
call .\scripts\code.bat %~dp0\..\extensions\azure\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azure --extensionTestsPath=%~dp0\..\extensions\azure\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR%
call .\scripts\code.bat %~dp0\..\extensions\azurecore\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR%
if %errorlevel% neq 0 exit /b %errorlevel%
call .\scripts\code.bat $%~dp0\..\extensions\emmet\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% .
if %errorlevel% neq 0 exit /b %errorlevel%
:: call .\scripts\code.bat $%~dp0\..\extensions\emmet\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% .
:: if %errorlevel% neq 0 exit /b %errorlevel%
:: Integration & performance tests in AMD
call .\scripts\test.bat --runGlob **\*.integrationTest.js %*
# Tests in commonJS (HTML, CSS, JSON language server tests...)
:: Tests in commonJS (HTML, CSS, JSON language server tests...)
call .\scripts\node-electron.bat .\node_modules\mocha\bin\_mocha .\extensions\*\server\out\test\**\*.test.js
if %errorlevel% neq 0 exit /b %errorlevel%

View File

@@ -17,7 +17,7 @@ cd $ROOT
# ./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started
./scripts/code.sh $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started
./scripts/code.sh $ROOT/extensions/markdown-language-features/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started
./scripts/code.sh $ROOT/extensions/azure/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azure --extensionTestsPath=$ROOT/extensions/azure/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started
./scripts/code.sh $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started
mkdir $ROOT/extensions/emmet/test-fixtures
./scripts/code.sh $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started .

View File

@@ -440,6 +440,13 @@ export class ScrollableSplitView extends HeightMap implements IDisposable {
size = typeof size === 'number' ? size : item.size;
size = clamp(size, item.view.minimumSize, item.view.maximumSize);
item.size = size;
this.updateSize(item.view.id, size);
let top = item.top + item.size;
for (let i = index + 1; i < this.viewItems.length; i++) {
let currentItem = this.viewItems[i];
this.updateTop(currentItem.view.id, top);
top += currentItem.size;
}
this.relayout(index);
}

View File

@@ -1,13 +1,16 @@
// Adapted from https://github.com/naresh-n/slickgrid-column-data-autosize/blob/master/src/slick.autocolumnsize.js
import { mixin, clone } from 'sql/base/common/objects';
import { isInDOM } from 'vs/base/browser/dom';
export interface IAutoColumnSizeOptions extends Slick.PluginOptions {
maxWidth?: number;
autoSizeOnRender?: boolean;
}
const defaultOptions: IAutoColumnSizeOptions = {
maxWidth: 200
maxWidth: 200,
autoSizeOnRender: false
};
export class AutoColumnSize<T> implements Slick.Plugin<T> {
@@ -15,6 +18,7 @@ export class AutoColumnSize<T> implements Slick.Plugin<T> {
private _$container: JQuery;
private _context: CanvasRenderingContext2D;
private _options: IAutoColumnSizeOptions;
private onPostEventHandler = new Slick.EventHandler();
constructor(options: IAutoColumnSizeOptions = defaultOptions) {
this._options = mixin(options, defaultOptions, false);
@@ -23,8 +27,12 @@ export class AutoColumnSize<T> implements Slick.Plugin<T> {
public init(grid: Slick.Grid<T>) {
this._grid = grid;
if (this._options.autoSizeOnRender) {
this.onPostEventHandler.subscribe(this._grid.onRendered, () => this.onPostRender());
}
this._$container = $(this._grid.getContainerNode());
this._$container.on('dblclick.autosize', '.slick-resizable-handle', e => this.reSizeColumn(e));
this._$container.on('dblclick.autosize', '.slick-resizable-handle', e => this.handleDoubleClick(e));
this._context = document.createElement('canvas').getContext('2d');
}
@@ -32,7 +40,66 @@ export class AutoColumnSize<T> implements Slick.Plugin<T> {
this._$container.off();
}
private reSizeColumn(e: JQuery.Event<HTMLElement, string>) {
private onPostRender() {
// this doesn't do anything if the grid isn't on the dom
if (!isInDOM(this._grid.getContainerNode())) {
return;
}
// since data can be async we want to only do this if we have the data to actual
// work on since we are measuring the physical length of data
let data = this._grid.getData();
let item = data.getItem(0);
if (item && Object.keys(item).length > 0) {
let hasValue = false;
for (let key in item) {
if (item.hasOwnProperty(key)) {
if (item[key]) {
hasValue = true;
break;
}
}
}
if (!hasValue) {
return;
}
} else {
return;
}
let headerColumnsQuery = $(this._grid.getContainerNode()).find('.slick-header-columns');
if (headerColumnsQuery && headerColumnsQuery.length) {
let headerColumns = headerColumnsQuery[0];
let origCols = this._grid.getColumns();
let allColumns = clone(origCols);
allColumns.forEach((col, index) => {
col.formatter = origCols[index].formatter;
col.asyncPostRender = origCols[index].asyncPostRender;
});
let change = false;
for (let i = 0; i <= headerColumns.children.length; i++) {
let headerEl = $(headerColumns.children.item(i));
let columnDef = headerEl.data('column');
if (columnDef) {
let headerWidth = this.getElementWidth(headerEl[0]);
let colIndex = this._grid.getColumnIndex(columnDef.id);
let column = allColumns[colIndex];
let autoSizeWidth = Math.max(headerWidth, this.getMaxColumnTextWidth(columnDef, colIndex)) + 1;
if (autoSizeWidth !== column.width) {
allColumns[colIndex].width = autoSizeWidth;
change = true;
}
}
}
if (change) {
this.onPostEventHandler.unsubscribeAll();
this._grid.setColumns(allColumns);
this._grid.onColumnsResized.notify();
}
}
}
private handleDoubleClick(e: JQuery.Event<HTMLElement, string>) {
let headerEl = $(e.currentTarget).closest('.slick-header-column');
let columnDef = headerEl.data('column');
@@ -43,6 +110,10 @@ export class AutoColumnSize<T> implements Slick.Plugin<T> {
e.preventDefault();
e.stopPropagation();
this.reSizeColumn(headerEl, columnDef);
}
private reSizeColumn(headerEl: JQuery, columnDef: Slick.Column<T>) {
let headerWidth = this.getElementWidth(headerEl[0]);
let colIndex = this._grid.getColumnIndex(columnDef.id);
let origCols = this._grid.getColumns();

View File

@@ -47,7 +47,7 @@ export class CopyKeybind<T> implements Slick.Plugin<T> {
if (!isUndefinedOrNull(this.grid.getColumns()[0].selectable) && !this.grid.getColumns()[0].selectable) {
startColumn = 1;
}
ranges = [new Slick.Range(selectedRows[0], startColumn, selectedRows[selectedRows.length - 1], this.grid.getColumns().length)]
ranges = [new Slick.Range(selectedRows[0], startColumn, selectedRows[selectedRows.length - 1], this.grid.getColumns().length)];
}
this._onCopy.fire(ranges);
}

View File

@@ -349,6 +349,7 @@ export class RowDetailView {
//slick-cell to escape the cell overflow clipping.
//sneaky extra </div> inserted here-----------------v
/* tslint:disable:no-unexternalized-strings */
html.push("<div class='detailView-toggle collapse'></div></div>");
html.push("<div id='cellDetailView_", dataContext.id, "' class='dynamic-cell-detail' "); //apply custom css to detail
@@ -356,6 +357,7 @@ export class RowDetailView {
html.push("top:", rowHeight, "px'>"); //shift detail below 1st row
html.push("<div id='detailViewContainer_", dataContext.id, "' class='detail-container' style='max-height:" + (dataContext._height - rowHeight + bottomMargin) + "px'>"); //sub ctr for custom styling
html.push("<div id='innerDetailView_", dataContext.id, "'>", escape(dataContext._detailContent), "</div></div>");
/* tslint:enable:no-unexternalized-strings */
//&omit a final closing detail container </div> that would come next
return html.join('');
@@ -364,17 +366,21 @@ export class RowDetailView {
}
public resizeDetailView(item) {
if (!item) return;
if (!item) {
return;
}
// Grad each of the dom items
let mainContainer = document.getElementById('detailViewContainer_' + item.id);
let cellItem = document.getElementById('cellDetailView_' + item.id);
let inner = document.getElementById('innerDetailView_' + item.id);
if (!mainContainer || !cellItem || !inner) return;
if (!mainContainer || !cellItem || !inner) {
return;
}
for (let idx = 1; idx <= item._sizePadding; idx++) {
this._dataView.deleteItem(item.id + "." + idx);
this._dataView.deleteItem(item.id + '.' + idx);
}
let rowHeight = this._grid.getOptions().rowHeight; // height of a row
@@ -395,9 +401,9 @@ export class RowDetailView {
this._grid.getOptions().minRowBuffer = item._sizePadding + 3;
}
mainContainer.setAttribute("style", "max-height: " + item._height + "px");
mainContainer.setAttribute('style', 'max-height: ' + item._height + 'px');
if (cellItem) {
cellItem.setAttribute("style", "height: " + item._height + "px;top:" + rowHeight + "px");
cellItem.setAttribute('style', 'height: ' + item._height + 'px;top:' + rowHeight + 'px');
}
let idxParent = this._dataView.getIdxById(item.id);

View File

@@ -46,6 +46,9 @@ export class Table<T extends Slick.SlickData> extends Widget implements IThemabl
private _onClick = new Emitter<ITableMouseEvent>();
public readonly onClick: Event<ITableMouseEvent> = this._onClick.event;
private _onColumnResize = new Emitter<void>();
public readonly onColumnResize = this._onColumnResize.event;
constructor(parent: HTMLElement, configuration?: ITableConfiguration<T>, options?: Slick.GridOptions<T>) {
super();
if (!configuration || !configuration.dataProvider || isArray(configuration.dataProvider)) {
@@ -105,6 +108,7 @@ export class Table<T extends Slick.SlickData> extends Widget implements IThemabl
this.mapMouseEvent(this._grid.onContextMenu, this._onContextMenu);
this.mapMouseEvent(this._grid.onClick, this._onClick);
this._grid.onColumnsResized.subscribe(() => this._onColumnResize.fire());
}
private mapMouseEvent(slickEvent: Slick.Event<any>, emitter: Emitter<ITableMouseEvent>) {

View File

@@ -8,6 +8,7 @@ import { Observer } from 'rxjs/Observer';
import { Event, Emitter } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import * as types from 'vs/base/common/types';
import { compare as stringCompare } from 'vs/base/common/strings';
import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces';
@@ -19,7 +20,23 @@ export interface IFindPosition {
function defaultSort<T>(args: Slick.OnSortEventArgs<T>, data: Array<T>): Array<T> {
let field = args.sortCol.field;
let sign = args.sortAsc ? 1 : -1;
return data.sort((a, b) => (a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1)) * sign);
let comparer: (a, b) => number;
if (types.isString(data[0][field])) {
if (Number(data[0][field]) !== NaN) {
comparer = (a: number, b: number) => {
let anum = Number(a[field]);
let bnum = Number(b[field]);
return anum === bnum ? 0 : anum > bnum ? 1 : -1;
};
} else {
comparer = stringCompare;
}
} else {
comparer = (a: number, b: number) => {
return a[field] === b[field] ? 0 : (a[field] > b[field] ? 1 : -1);
};
}
return data.sort((a, b) => comparer(a, b) * sign);
}
export class TableDataView<T extends Slick.SlickData> implements IDisposableDataProvider<T> {

View File

@@ -11,7 +11,7 @@ export interface ICommandLineProcessing {
* Interprets the various Azure Data Studio-specific command line switches and
* performs the requisite tasks such as connecting to a server
*/
processCommandLine() : void;
processCommandLine() : Promise<void>;
}
export const ICommandLineProcessing = createDecorator<ICommandLineProcessing>('commandLineService');

View File

@@ -15,10 +15,13 @@ import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions a
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { warn } from 'sql/base/common/log';
export class CommandLineService implements ICommandLineProcessing {
private _connectionProfile: ConnectionProfile;
private _showConnectionDialog: boolean;
private _commandName:string;
constructor(
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@@ -27,49 +30,96 @@ export class CommandLineService implements ICommandLineProcessing {
@IQueryEditorService private _queryEditorService: IQueryEditorService,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService,
@IEditorService private _editorService: IEditorService,
@ICommandService private _commandService: ICommandService
) {
let profile = null;
if (this._environmentService && this._environmentService.args.server) {
profile = new ConnectionProfile(_capabilitiesService, null);
// We want connection store to use any matching password it finds
profile.savePassword = true;
profile.providerName = Constants.mssqlProviderName;
profile.serverName = _environmentService.args.server;
profile.databaseName = _environmentService.args.database ? _environmentService.args.database : '';
profile.userName = _environmentService.args.user ? _environmentService.args.user : '';
profile.authenticationType = _environmentService.args.integrated ? 'Integrated' : 'SqlLogin';
profile.connectionName = '';
profile.setOptionValue('applicationName', Constants.applicationName);
profile.setOptionValue('databaseDisplayName', profile.databaseName);
profile.setOptionValue('groupId', profile.groupId);
if (this._environmentService) {
if (this._commandService) {
this._commandName = this._environmentService.args.command;
}
if (this._environmentService.args.server) {
profile = new ConnectionProfile(_capabilitiesService, null);
// We want connection store to use any matching password it finds
profile.savePassword = true;
profile.providerName = Constants.mssqlProviderName;
profile.serverName = _environmentService.args.server;
profile.databaseName = _environmentService.args.database ? _environmentService.args.database : '';
profile.userName = _environmentService.args.user ? _environmentService.args.user : '';
profile.authenticationType = _environmentService.args.integrated ? 'Integrated' : 'SqlLogin';
profile.connectionName = '';
profile.setOptionValue('applicationName', Constants.applicationName);
profile.setOptionValue('databaseDisplayName', profile.databaseName);
profile.setOptionValue('groupId', profile.groupId);
}
}
this._connectionProfile = profile;
const registry = platform.Registry.as<IConnectionProviderRegistry>(ConnectionProviderExtensions.ConnectionProviderContributions);
let sqlProvider = registry.getProperties(Constants.mssqlProviderName);
// We can't connect to object explorer until the MSSQL connection provider is registered
if (sqlProvider) {
this.processCommandLine();
this.processCommandLine().catch(reason=>{warn('processCommandLine failed: ' + reason);});
} else {
registry.onNewProvider(e => {
if (e.id === Constants.mssqlProviderName) {
this.processCommandLine();
this.processCommandLine().catch(reason=>{warn('processCommandLine failed: ' + reason);});
}
});
}
}
public _serviceBrand: any;
public processCommandLine(): void {
if (!this._connectionProfile && !this._connectionManagementService.hasRegisteredServers()) {
// prompt the user for a new connection on startup if no profiles are registered
this._connectionManagementService.showConnectionDialog();
} else if (this._connectionProfile) {
this._connectionManagementService.connectIfNotConnected(this._connectionProfile, 'connection', true)
.then(result => TaskUtilities.newQuery(this._connectionProfile,
this._connectionManagementService,
this._queryEditorService,
this._objectExplorerService,
this._editorService))
.catch(() => { });
}
// We base our logic on the combination of (server, command) values.
// (serverName, commandName) => Connect object explorer and execute the command, passing the connection profile to the command. Do not load query editor.
// (null, commandName) => Launch the command with a null connection. If the command implementation needs a connection, it will need to create it.
// (serverName, null) => Connect object explorer and open a new query editor
// (null, null) => Prompt for a connection unless there are registered servers
public processCommandLine(): Promise<void> {
let self = this;
return new Promise<void>((resolve, reject) => {
if (!self._commandName && !self._connectionProfile && !self._connectionManagementService.hasRegisteredServers()) {
// prompt the user for a new connection on startup if no profiles are registered
self._connectionManagementService.showConnectionDialog()
.then(() => {
resolve();
},
error => {
reject(error);
});
} else if (self._connectionProfile) {
if (!self._commandName) {
self._connectionManagementService.connectIfNotConnected(self._connectionProfile, 'connection', true)
.then(() => {
TaskUtilities.newQuery(self._connectionProfile,
self._connectionManagementService,
self._queryEditorService,
self._objectExplorerService,
self._editorService)
.then( () => {
resolve();
}, error => {
// ignore query editor failing to open.
// the tests don't mock this out
warn('unable to open query editor ' + error);
resolve();
});
}, error => {
reject(error);
});
} else {
self._connectionManagementService.connectIfNotConnected(self._connectionProfile, 'connection', true)
.then(() => {
self._commandService.executeCommand(self._commandName, self._connectionProfile).then(() => resolve(), error => reject(error));
}, error => {
reject(error);
});
}
} else if (self._commandName) {
self._commandService.executeCommand(self._commandName).then(() => resolve(), error => reject(error));
}
else {
resolve();
}
});
}
}

View File

@@ -17,7 +17,7 @@ import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput';
import { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/services/notebook/notebookService';
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
import { getProvidersForFileName } from 'sql/parts/notebook/notebookUtils';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
const fs = require('fs');
@@ -63,13 +63,14 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti
if (uri && notebookValidator.isNotebookEnabled()) {
return withService<INotebookService, NotebookInput>(instantiationService, INotebookService, notebookService => {
let fileName: string = 'untitled';
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
let providerIds: string[] = [DEFAULT_NOTEBOOK_PROVIDER];
if (input) {
fileName = input.getName();
providerId = getProviderForFileName(fileName, notebookService);
providerIds = getProvidersForFileName(fileName, notebookService);
}
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
notebookInputModel.providerId = providerId;
notebookInputModel.providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0];
notebookInputModel.providers = providerIds;
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
return notebookInput;
});

View File

@@ -88,7 +88,7 @@ export function parseNumAsTimeString(value: number, includeFraction: boolean = t
return tempVal > 0 && includeFraction ? rs + '.' + mss : rs;
}
export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): string {
export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection' | 'notebook'): string {
let prefix = purpose ? uriPrefixes[purpose] : uriPrefixes.default;
let uri = generateUriWithPrefix(connection, prefix);

View File

@@ -17,7 +17,6 @@ import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
import * as Constants from 'sql/parts/connection/common/constants';
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup';
import { attachButtonStyler, attachCheckboxStyler, attachEditableDropdownStyler, attachInputBoxStyler } from 'sql/common/theme/styler';
import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
@@ -290,14 +289,14 @@ export class ConnectionWidget {
private registerListeners(): void {
// Theme styler
this._toDispose.push(attachInputBoxStyler(this._serverNameInputBox, this._themeService));
this._toDispose.push(attachEditableDropdownStyler(this._databaseNameInputBox, this._themeService));
this._toDispose.push(attachInputBoxStyler(this._connectionNameInputBox, this._themeService));
this._toDispose.push(attachInputBoxStyler(this._userNameInputBox, this._themeService));
this._toDispose.push(attachInputBoxStyler(this._passwordInputBox, this._themeService));
this._toDispose.push(styler.attachInputBoxStyler(this._serverNameInputBox, this._themeService));
this._toDispose.push(styler.attachEditableDropdownStyler(this._databaseNameInputBox, this._themeService));
this._toDispose.push(styler.attachInputBoxStyler(this._connectionNameInputBox, this._themeService));
this._toDispose.push(styler.attachInputBoxStyler(this._userNameInputBox, this._themeService));
this._toDispose.push(styler.attachInputBoxStyler(this._passwordInputBox, this._themeService));
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.attachButtonStyler(this._advancedButton, this._themeService));
this._toDispose.push(styler.attachCheckboxStyler(this._rememberPasswordCheckBox, this._themeService));
this._toDispose.push(styler.attachSelectBoxStyler(this._azureAccountDropdown, this._themeService));
if (this._authTypeSelectBox) {

View File

@@ -90,6 +90,8 @@
margin: 0px 13px;
}
.vs-dark .connection-dialog .connection-history-actions .action-label.icon,
.hc-black .connection-dialog .connection-history-actions .action-label.icon,
.connection-dialog .connection-history-actions .action-label.icon {
display: block;
height: 20px;

View File

@@ -17,7 +17,7 @@ import * as nls from 'vs/nls';
export enum BreadcrumbClass {
DatabasePage,
ServerPage
};
}
@Injectable()
export class BreadcrumbService implements IBreadcrumbService {

View File

@@ -109,7 +109,7 @@ export class BackupUiService implements IBackupUiService {
let self = this;
return new Promise<void>((resolve, reject) => {
self.showBackupDialog(connection).then(() => {
resolve();
resolve(void 0);
}, error => {
reject();
});
@@ -145,7 +145,7 @@ export class BackupUiService implements IBackupUiService {
}
let backupOptions = this.getOptions(this._currentProvider);
return new TPromise<void>(() => {
return new TPromise<void>((resolve) => {
let uri = this._connectionManagementService.getConnectionUri(connection)
+ ProviderConnectionInfo.idSeparator
+ ConnectionUtils.ConnectionUriBackupIdAttributeName
@@ -168,6 +168,7 @@ export class BackupUiService implements IBackupUiService {
} else {
(backupDialog as BackupDialog).open(connection);
}
resolve(void 0);
});
}

View File

@@ -22,25 +22,25 @@ export const recoveryModelSimple = 'Simple';
export const recoveryModelFull = 'Full';
// Constants for UI strings
export const labelDatabase = 'Database';
export const labelFilegroup = 'Files and filegroups';
export const labelFull = 'Full';
export const labelDifferential = 'Differential';
export const labelLog = 'Transaction Log';
export const labelDisk = 'Disk';
export const labelUrl = 'Url';
export const labelDatabase = localize('backup.labelDatabase', 'Database');
export const labelFilegroup = localize('backup.labelFilegroup', 'Files and filegroups');
export const labelFull = localize('backup.labelFull', 'Full');
export const labelDifferential = localize('backup.labelDifferential', 'Differential');
export const labelLog = localize('backup.labelLog', 'Transaction Log');
export const labelDisk = localize('backup.labelDisk', 'Disk');
export const labelUrl = localize('backup.labelUrl', 'Url');
export const defaultCompression = 'Use the default server setting';
export const compressionOn = 'Compress backup';
export const compressionOff = 'Do not compress backup';
export const defaultCompression = localize('backup.defaultCompression', 'Use the default server setting');
export const compressionOn = localize('backup.compressBackup', 'Compress backup');
export const compressionOff = localize('backup.doNotCompress', 'Do not compress backup');
export const aes128 = 'AES 128';
export const aes192 = 'AES 192';
export const aes256 = 'AES 256';
export const tripleDES = 'Triple DES';
export const serverCertificate = "Server Certificate";
export const asymmetricKey = "Asymmetric Key";
export const serverCertificate = localize('backup.serverCertificate', "Server Certificate");
export const asymmetricKey = localize('backup.asymmetricKey', "Asymmetric Key");
export const fileFiltersSet: {label: string, filters: string[]}[] = [
{ label: localize('backup.filterBackupFiles', "Backup Files"), filters: ['*.bak', '*.trn', '*.log'] },

View File

@@ -7,5 +7,5 @@
* List of services that provide file validation callback to file browser service
*/
export const backup: string = "Backup";
export const restore: string = "Restore";
export const backup: string = 'Backup';
export const restore: string = 'Restore';

View File

@@ -48,13 +48,13 @@ export function textFormatter(row: number, cell: any, value: any, columnDef: any
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;
titleValue = valueToDisplay;
} else {
cellClasses += ' missing-value';
}
} else if (typeof value === 'string') {
valueToDisplay = escape(value.length > 250 ? value.slice(0, 250) + '...' : value);
titleValue = value;
titleValue = valueToDisplay;
}
return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`;

View File

@@ -343,6 +343,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
handleResultSet(self: EditDataComponent, event: any): void {
// Clone the data before altering it to avoid impacting other subscribers
let resultSet = Object.assign({}, event.data);
if (!resultSet.complete) {
return;
}
// Add an extra 'new row'
resultSet.rowCount++;

View File

@@ -412,7 +412,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
};
sibling.onmouseleave = (e) => {
targetChildren.removeClass('hovered');
}
};
break;
}
}

View File

@@ -131,7 +131,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
}
public get showRadioButton():boolean{
return this.selectable && (this.selected || this._hasFocus)
return this.selectable && (this.selected || this._hasFocus);
}
public get showAsSelected(): boolean {

View File

@@ -38,9 +38,9 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges {
ngOnInit() {
if (this.cellModel) {
this.cellModel.onOutputsChanged(() => {
this._register(this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges();
});
}));
}
}

View File

@@ -33,9 +33,9 @@ export class OutputAreaComponent extends AngularDisposable implements OnInit {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
if (this.cellModel) {
this.cellModel.onOutputsChanged(() => {
this._register(this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges();
});
}));
}
}

View File

@@ -91,9 +91,9 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
this.setLoading(false);
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.cellModel.onOutputsChanged(e => {
this._register(this.cellModel.onOutputsChanged(e => {
this.updatePreview();
});
}));
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {

View File

@@ -226,6 +226,9 @@ export class CellModel implements ICellModel {
this._outputs.push(this.rewriteOutputUrls(output));
this.fireOutputsChanged();
}
if (!this._future.inProgress) {
this._future.dispose();
}
}
private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput {
@@ -327,6 +330,7 @@ export class CellModel implements ICellModel {
CellModel.LanguageMapping['pyspark3'] = 'python';
CellModel.LanguageMapping['python'] = 'python';
CellModel.LanguageMapping['scala'] = 'scala';
CellModel.LanguageMapping['sql'] = 'sql';
}
private get languageInfo(): nb.ILanguageInfo {

View File

@@ -15,7 +15,7 @@ export declare type JSONValue = JSONPrimitive | JSONObject | JSONArray;
* A type definition for a JSON object.
*/
export interface JSONObject {
[key: string]: JSONValue;
[key: string]: JSONValue;
}
/**
* A type definition for a JSON array.
@@ -26,7 +26,7 @@ export interface JSONArray extends Array<JSONValue> {
* A type definition for a readonly JSON object.
*/
export interface ReadonlyJSONObject {
readonly [key: string]: ReadonlyJSONValue;
readonly [key: string]: ReadonlyJSONValue;
}
/**
* A type definition for a readonly JSON array.
@@ -45,10 +45,10 @@ export declare type ReadonlyJSONValue = JSONPrimitive | ReadonlyJSONObject | Rea
* @returns `true` if the value is a primitive,`false` otherwise.
*/
export function isPrimitive(value: any): boolean {
return (
value === null ||
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'string'
);
return (
value === null ||
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'string'
);
}

View File

@@ -249,9 +249,9 @@ export interface INotebookModel {
readonly languageInfo: nb.ILanguageInfo;
/**
* The notebook service used to call backend APIs
* All notebook managers applicable for a given notebook
*/
readonly notebookManager: INotebookManager;
readonly notebookManagers: INotebookManager[];
/**
* Event fired on first initialization of the kernel and
@@ -299,6 +299,11 @@ export interface INotebookModel {
*/
trustedMode: boolean;
/**
* Current notebook provider id
*/
providerId: string;
/**
* Change the current kernel from the Kernel dropdown
* @param displayName kernel name (as displayed in Kernel dropdown)
@@ -411,7 +416,8 @@ export interface INotebookModelOptions {
*/
factory: IModelFactory;
notebookManager: INotebookManager;
notebookManagers: INotebookManager[];
providerId: string;
notificationService: INotificationService;
connectionService: IConnectionManagementService;

View File

@@ -0,0 +1,494 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
// Notebook format interfaces
// https://nbformat.readthedocs.io/en/latest/format_description.html
// https://github.com/jupyter/nbformat/blob/master/nbformat/v4/nbformat.v4.schema.json
import { JSONObject } from './jsonext';
import { nb } from 'sqlops';
/**
* A namespace for nbformat interfaces.
*/
export namespace nbformat {
/**
* The major version of the notebook format.
*/
export const MAJOR_VERSION: number = 4;
/**
* The minor version of the notebook format.
*/
export const MINOR_VERSION: number = 2;
/**
* The kernelspec metadata.
*/
export interface IKernelspecMetadata extends JSONObject {
name: string;
display_name: string;
}
/**
* The language info metatda
*/
export interface ILanguageInfoMetadata extends JSONObject {
name: string;
codemirror_mode?: string | JSONObject;
file_extension?: string;
mimetype?: string;
pygments_lexer?: string;
}
/**
* The default metadata for the notebook.
*/
export interface INotebookMetadata extends JSONObject {
kernelspec?: IKernelspecMetadata;
language_info?: ILanguageInfoMetadata;
orig_nbformat: number;
}
/**
* The notebook content.
*/
export interface INotebookContent {
metadata: INotebookMetadata;
nbformat_minor: number;
nbformat: number;
cells: ICell[];
}
/**
* A multiline string.
*/
export type MultilineString = string | string[];
/**
* A mime-type keyed dictionary of data.
*/
export interface IMimeBundle extends JSONObject {
[key: string]: MultilineString | JSONObject;
}
/**
* Media attachments (e.g. inline images).
*/
export interface IAttachments {
[key: string]: IMimeBundle;
}
/**
* The code cell's prompt number. Will be null if the cell has not been run.
*/
export type ExecutionCount = number | null;
/**
* Cell output metadata.
*/
export type OutputMetadata = JSONObject;
/**
* Validate a mime type/value pair.
*
* @param type - The mimetype name.
*
* @param value - The value associated with the type.
*
* @returns Whether the type/value pair are valid.
*/
export function validateMimeValue(
type: string,
value: MultilineString | JSONObject
): boolean {
// Check if "application/json" or "application/foo+json"
const jsonTest = /^application\/(.*?)+\+json$/;
const isJSONType = type === 'application/json' || jsonTest.test(type);
let isString = (x: any) => {
return Object.prototype.toString.call(x) === '[object String]';
};
// If it is an array, make sure if is not a JSON type and it is an
// array of strings.
if (Array.isArray(value)) {
if (isJSONType) {
return false;
}
let valid = true;
(value as string[]).forEach(v => {
if (!isString(v)) {
valid = false;
}
});
return valid;
}
// If it is a string, make sure we are not a JSON type.
if (isString(value)) {
return !isJSONType;
}
// It is not a string, make sure it is a JSON type.
if (!isJSONType) {
return false;
}
// It is a JSON type, make sure it is a valid JSON object.
// return JSONExt.isObject(value);
return true;
}
/**
* Cell-level metadata.
*/
export interface IBaseCellMetadata extends JSONObject {
/**
* Whether the cell is trusted.
*
* #### Notes
* This is not strictly part of the nbformat spec, but it is added by
* the contents manager.
*
* See https://jupyter-notebook.readthedocs.io/en/latest/security.html.
*/
trusted: boolean;
/**
* The cell's name. If present, must be a non-empty string.
*/
name: string;
/**
* The cell's tags. Tags must be unique, and must not contain commas.
*/
tags: string[];
}
/**
* The base cell interface.
*/
export interface IBaseCell {
/**
* String identifying the type of cell.
*/
cell_type: string;
/**
* Contents of the cell, represented as an array of lines.
*/
source: MultilineString;
/**
* Cell-level metadata.
*/
metadata: Partial<ICellMetadata>;
}
/**
* Metadata for the raw cell.
*/
export interface IRawCellMetadata extends IBaseCellMetadata {
/**
* Raw cell metadata format for nbconvert.
*/
format: string;
}
/**
* A raw cell.
*/
export interface IRawCell extends IBaseCell {
/**
* String identifying the type of cell.
*/
cell_type: 'raw';
/**
* Cell-level metadata.
*/
metadata: Partial<IRawCellMetadata>;
/**
* Cell attachments.
*/
attachments?: IAttachments;
}
/**
* A markdown cell.
*/
export interface IMarkdownCell extends IBaseCell {
/**
* String identifying the type of cell.
*/
cell_type: 'markdown';
/**
* Cell attachments.
*/
attachments?: IAttachments;
}
/**
* Metadata for a code cell.
*/
export interface ICodeCellMetadata extends IBaseCellMetadata {
/**
* Whether the cell is collapsed/expanded.
*/
collapsed: boolean;
/**
* Whether the cell's output is scrolled, unscrolled, or autoscrolled.
*/
scrolled: boolean | 'auto';
}
/**
* A code cell.
*/
export interface ICodeCell extends IBaseCell {
/**
* String identifying the type of cell.
*/
cell_type: 'code';
/**
* Cell-level metadata.
*/
metadata: Partial<ICodeCellMetadata>;
/**
* Execution, display, or stream outputs.
*/
outputs: IOutput[];
/**
* The code cell's prompt number. Will be null if the cell has not been run.
*/
execution_count: ExecutionCount;
}
/**
* An unrecognized cell.
*/
export interface IUnrecognizedCell extends IBaseCell { }
/**
* A cell union type.
*/
export type ICell = IRawCell | IMarkdownCell | ICodeCell | IUnrecognizedCell;
/**
* Test whether a cell is a raw cell.
*/
export function isRaw(cell: ICell): cell is IRawCell {
return cell.cell_type === 'raw';
}
/**
* Test whether a cell is a markdown cell.
*/
export function isMarkdown(cell: ICell): cell is IMarkdownCell {
return cell.cell_type === 'markdown';
}
/**
* Test whether a cell is a code cell.
*/
export function isCode(cell: ICell): cell is ICodeCell {
return cell.cell_type === 'code';
}
/**
* A union metadata type.
*/
export type ICellMetadata =
| IBaseCellMetadata
| IRawCellMetadata
| ICodeCellMetadata;
/**
* The valid output types.
*/
export type OutputType =
| 'execute_result'
| 'display_data'
| 'stream'
| 'error'
| 'update_display_data';
/**
* Result of executing a code cell.
*/
export interface IExecuteResult extends nb.ICellOutput {
/**
* Type of cell output.
*/
output_type: 'execute_result';
/**
* A result's prompt number.
*/
execution_count: ExecutionCount;
/**
* A mime-type keyed dictionary of data.
*/
data: IMimeBundle;
/**
* Cell output metadata.
*/
metadata: OutputMetadata;
}
/**
* Data displayed as a result of code cell execution.
*/
export interface IDisplayData extends nb.ICellOutput {
/**
* Type of cell output.
*/
output_type: 'display_data';
/**
* A mime-type keyed dictionary of data.
*/
data: IMimeBundle;
/**
* Cell output metadata.
*/
metadata: OutputMetadata;
}
/**
* Data displayed as an update to existing display data.
*/
export interface IDisplayUpdate extends nb.ICellOutput {
/**
* Type of cell output.
*/
output_type: 'update_display_data';
/**
* A mime-type keyed dictionary of data.
*/
data: IMimeBundle;
/**
* Cell output metadata.
*/
metadata: OutputMetadata;
}
/**
* Stream output from a code cell.
*/
export interface IStream extends nb.ICellOutput {
/**
* Type of cell output.
*/
output_type: 'stream';
/**
* The name of the stream.
*/
name: StreamType;
/**
* The stream's text output.
*/
text: MultilineString;
}
/**
* An alias for a stream type.
*/
export type StreamType = 'stdout' | 'stderr';
/**
* Output of an error that occurred during code cell execution.
*/
export interface IError extends nb.ICellOutput {
/**
* Type of cell output.
*/
output_type: 'error';
/**
* The name of the error.
*/
ename: string;
/**
* The value, or message, of the error.
*/
evalue: string;
/**
* The error's traceback.
*/
traceback: string[];
}
/**
* Unrecognized output.
*/
export interface IUnrecognizedOutput extends nb.ICellOutput { }
/**
* Test whether an output is an execute result.
*/
export function isExecuteResult(output: IOutput): output is IExecuteResult {
return output.output_type === 'execute_result';
}
/**
* Test whether an output is from display data.
*/
export function isDisplayData(output: IOutput): output is IDisplayData {
return output.output_type === 'display_data';
}
/**
* Test whether an output is from updated display data.
*/
export function isDisplayUpdate(output: IOutput): output is IDisplayUpdate {
return output.output_type === 'update_display_data';
}
/**
* Test whether an output is from a stream.
*/
export function isStream(output: IOutput): output is IStream {
return output.output_type === 'stream';
}
/**
* Test whether an output is from a stream.
*/
export function isError(output: IOutput): output is IError {
return output.output_type === 'error';
}
/**
* An output union type.
*/
export type IOutput =
| IUnrecognizedOutput
| IExecuteResult
| IDisplayData
| IStream
| IError;
}
export interface ICellOutputWithIdAndTrust extends nb.ICellOutput {
id: number;
trusted: boolean;
}

View File

@@ -16,7 +16,7 @@ import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptio
import { NotebookChangeType, CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
import { nbversion } from '../notebookConstants';
import * as notebookUtils from '../notebookUtils';
import { INotebookManager } from 'sql/services/notebook/notebookService';
import { INotebookManager, SQL_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
@@ -45,7 +45,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
private _contentChangedEmitter = new Emitter<NotebookContentChange>();
private _kernelsChangedEmitter = new Emitter<nb.IKernelSpec>();
private _inErrorState: boolean = false;
private _clientSession: IClientSession;
private _clientSessions: IClientSession[] = [];
private _activeClientSession: IClientSession;
private _sessionLoadFinished: Promise<void>;
private _onClientSessionReady = new Emitter<IClientSession>();
private _activeContexts: IDefaultConnection;
@@ -60,20 +61,30 @@ export class NotebookModel extends Disposable implements INotebookModel {
private _hadoopConnection: NotebookConnection;
private _defaultKernel: nb.IKernelSpec;
private _activeCell: ICellModel;
private _providerId: string;
constructor(private notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, private connectionProfile?: IConnectionProfile) {
super();
if (!notebookOptions || !notebookOptions.notebookUri || !notebookOptions.notebookManager) {
if (!notebookOptions || !notebookOptions.notebookUri || !notebookOptions.notebookManagers) {
throw new Error('path or notebook service not defined');
}
if (startSessionImmediately) {
this.backgroundStartSession();
}
this._trustedMode = false;
this._providerId = notebookOptions.providerId;
}
public get notebookManagers(): INotebookManager[] {
let notebookManagers = this.notebookOptions.notebookManagers.filter(manager => manager.providerId !== DEFAULT_NOTEBOOK_PROVIDER);
if (!notebookManagers.length) {
return this.notebookOptions.notebookManagers;
}
return notebookManagers;
}
public get notebookManager(): INotebookManager {
return this.notebookOptions.notebookManager;
return this.notebookManagers.find(manager => manager.providerId === this._providerId);
}
public get notebookUri() : URI {
@@ -93,7 +104,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
public get isSessionReady(): boolean {
return !!this._clientSession;
return !!this._activeClientSession;
}
/**
@@ -102,11 +113,11 @@ export class NotebookModel extends Disposable implements INotebookModel {
* notebook environment
*/
public get clientSession(): IClientSession {
return this._clientSession;
return this._activeClientSession;
}
public get kernelChanged(): Event<nb.IKernelChangedArgs> {
return this.clientSession.kernelChanged;
return this._activeClientSession.kernelChanged;
}
public get kernelsChanged(): Event<nb.IKernelSpec> {
@@ -130,7 +141,21 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
public get specs(): nb.IAllKernels | undefined {
return this.notebookManager.sessionManager.specs;
let specs: nb.IAllKernels = {
defaultKernel: undefined,
kernels: []
};
this.notebookManagers.forEach(manager => {
if (manager.sessionManager && manager.sessionManager.specs && manager.sessionManager.specs.kernels) {
manager.sessionManager.specs.kernels.forEach(kernel => {
specs.kernels.push(kernel);
});
if (!specs.defaultKernel) {
specs.defaultKernel = manager.sessionManager.specs.defaultKernel;
}
}
});
return specs;
}
public get inErrorState(): boolean {
@@ -145,6 +170,10 @@ export class NotebookModel extends Disposable implements INotebookModel {
return this._trustedMode;
}
public get providerId(): string {
return this._providerId;
}
public set trustedMode(isTrusted: boolean) {
this._trustedMode = isTrusted;
if (this._cells) {
@@ -178,11 +207,16 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._trustedMode = isTrusted;
let contents = null;
if (this.notebookOptions.notebookUri.scheme !== Schemas.untitled) {
contents = await this.notebookManager.contentManager.getNotebookContents(this.notebookOptions.notebookUri);
// TODO: separate ContentManager from NotebookManager
contents = await this.notebookManagers[0].contentManager.getNotebookContents(this.notebookOptions.notebookUri);
}
let factory = this.notebookOptions.factory;
// if cells already exist, create them with language info (if it is saved)
this._cells = undefined;
this._defaultLanguageInfo = {
name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python',
version: ''
};
if (contents) {
this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents);
this._savedKernelInfo = this.getSavedKernelInfo(contents);
@@ -203,9 +237,9 @@ export class NotebookModel extends Disposable implements INotebookModel {
return this._cells.findIndex((cell) => cell.equals(cellModel));
}
public addCell(cellType: CellType, index?: number): void {
public addCell(cellType: CellType, index?: number): ICellModel {
if (this.inErrorState || !this._cells) {
return;
return null;
}
let cell = this.createCell(cellType);
@@ -215,12 +249,18 @@ export class NotebookModel extends Disposable implements INotebookModel {
this._cells.push(cell);
index = undefined;
}
// Set newly created cell as active cell
this._activeCell.active = false;
this._activeCell = cell;
this._activeCell.active = true;
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.CellsAdded,
cells: [cell],
cellIndex: index
});
return cell;
}
private createCell(cellType: CellType): ICellModel {
@@ -282,29 +322,36 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
public backgroundStartSession(): void {
this._clientSession = this.notebookOptions.factory.createClientSession({
notebookUri: this.notebookOptions.notebookUri,
notebookManager: this.notebookManager,
notificationService: this.notebookOptions.notificationService
});
let profile = this.connectionProfile as IConnectionProfile;
if (this.isValidKnoxConnection(profile)) {
this._hadoopConnection = new NotebookConnection(this.connectionProfile);
} else {
this._hadoopConnection = undefined;
}
this._clientSession.initialize(this._hadoopConnection);
this._sessionLoadFinished = this._clientSession.ready.then(async () => {
if (this._clientSession.isInErrorState) {
this.setErrorState(this._clientSession.errorMessage);
} else {
this._onClientSessionReady.fire(this._clientSession);
// Once session is loaded, can use the session manager to retrieve useful info
this.loadKernelInfo();
await this.loadActiveContexts(undefined);
// TODO: only one session should be active at a time, depending on the current provider
this.notebookManagers.forEach(manager => {
let clientSession = this.notebookOptions.factory.createClientSession({
notebookUri: this.notebookOptions.notebookUri,
notebookManager: manager,
notificationService: this.notebookOptions.notificationService
});
this._clientSessions.push(clientSession);
if (!this._activeClientSession) {
this._activeClientSession = clientSession;
}
let profile = this.connectionProfile as IConnectionProfile;
if (this.isValidKnoxConnection(profile)) {
this._hadoopConnection = new NotebookConnection(this.connectionProfile);
} else {
this._hadoopConnection = undefined;
}
clientSession.initialize(this._hadoopConnection);
this._sessionLoadFinished = clientSession.ready.then(async () => {
if (clientSession.isInErrorState) {
this.setErrorState(clientSession.errorMessage);
} else {
this._onClientSessionReady.fire(clientSession);
// Once session is loaded, can use the session manager to retrieve useful info
this.loadKernelInfo();
await this.loadActiveContexts(undefined);
}
});
});
}
@@ -328,7 +375,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
public doChangeKernel(kernelSpec: nb.IKernelSpec): Promise<void> {
return this._clientSession.changeKernel(kernelSpec)
this.findProviderIdForKernel(kernelSpec);
return this._activeClientSession.changeKernel(kernelSpec)
.then((kernel) => {
kernel.ready.then(() => {
if (kernel.info) {
@@ -353,7 +401,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
SparkMagicContexts.configureContext(this.notebookOptions);
this._hadoopConnection = new NotebookConnection(newConnection);
this.refreshConnections(newConnection);
this._clientSession.updateConnection(this._hadoopConnection);
this._activeClientSession.updateConnection(this._hadoopConnection);
} catch (err) {
let msg = notebookUtils.getErrorMessage(err);
this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg));
@@ -375,20 +423,27 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
private loadKernelInfo(): void {
this.clientSession.kernelChanged(async (e) => {
await this.loadActiveContexts(e);
this._clientSessions.forEach(clientSession => {
clientSession.kernelChanged(async (e) => {
await this.loadActiveContexts(e);
});
});
if (!this.notebookManager) {
return;
}
try {
let sessionManager = this.notebookManager.sessionManager;
if (sessionManager) {
let defaultKernel = SparkMagicContexts.getDefaultKernel(sessionManager.specs, this.connectionProfile, this._savedKernelInfo, this.notebookOptions.notificationService);
this._defaultKernel = defaultKernel;
this._clientSession.statusChanged(async (session) => {
if (session && session.defaultKernelLoaded === true) {
this._kernelsChangedEmitter.fire(defaultKernel);
} else if (session && !session.defaultKernelLoaded) {
this._kernelsChangedEmitter.fire({ name: notebookConstants.python3, display_name: notebookConstants.python3DisplayName });
}
this._clientSessions.forEach(clientSession => {
clientSession.statusChanged(async (session) => {
if (session && session.defaultKernelLoaded === true) {
this._kernelsChangedEmitter.fire(defaultKernel);
} else if (session && !session.defaultKernelLoaded) {
this._kernelsChangedEmitter.fire({ name: notebookConstants.python3, display_name: notebookConstants.python3DisplayName });
}
});
});
this.doChangeKernel(defaultKernel);
}
@@ -402,9 +457,9 @@ export class NotebookModel extends Disposable implements INotebookModel {
// Otherwise, default to python
private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo {
return notebook!.metadata!.language_info || {
name: 'python',
name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python',
version: '',
mimetype: 'x-python'
mimetype: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'x-sql' : 'x-python'
};
}
@@ -438,9 +493,10 @@ export class NotebookModel extends Disposable implements INotebookModel {
public async handleClosed(): Promise<void> {
try {
if (this._clientSession) {
await this._clientSession.shutdown();
this._clientSession = undefined;
if (this._activeClientSession) {
await this._activeClientSession.shutdown();
this._clientSessions = undefined;
this._activeClientSession = undefined;
}
} catch (err) {
this.notifyError(localize('shutdownError', 'An error occurred when closing the notebook: {0}', err));
@@ -476,7 +532,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
if (!notebook) {
return false;
}
await this.notebookManager.contentManager.save(this.notebookOptions.notebookUri, notebook);
// TODO: refactor ContentManager out from NotebookManager
await this.notebookManagers[0].contentManager.save(this.notebookOptions.notebookUri, notebook);
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.DirtyStateChanged,
isDirty: false
@@ -498,6 +555,23 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
}
}
/**
* Set _providerId and _activeClientSession based on a kernelSpec representing new kernel
* @param kernelSpec KernelSpec for new kernel
*/
private findProviderIdForKernel(kernelSpec: nb.IKernelSpec): void {
for (let i = 0; i < this.notebookManagers.length; i++) {
if (this.notebookManagers[i].sessionManager && this.notebookManagers[i].sessionManager.specs && this.notebookManagers[i].sessionManager.specs.kernels) {
let index = this.notebookManagers[i].sessionManager.specs.kernels.findIndex(kernel => kernel.name === kernelSpec.name);
if (index >= 0) {
this._activeClientSession = this._clientSessions[i];
this._providerId = this.notebookManagers[i].providerId;
break;
}
}
}
}
/**
* Serialize the model to JSON.
*/

View File

@@ -34,7 +34,7 @@ import { AngularDisposable } from 'sql/base/common/lifecycle';
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
import { ICellModel, IModelFactory, notebookConstants, INotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces';
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_FILETYPE, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_FILETYPE, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
@@ -63,14 +63,12 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
private _isInErrorState: boolean = false;
private _errorMessage: string;
protected _actionBar: Taskbar;
private _activeCell: ICellModel;
protected isLoading: boolean;
private notebookManager: INotebookManager;
private notebookManagers: INotebookManager[] = [];
private _modelReadyDeferred = new Deferred<NotebookModel>();
private _modelRegisteredDeferred = new Deferred<NotebookModel>();
private profile: IConnectionProfile;
private _trustedAction: TrustedAction;
private _activeCellId: string;
constructor(
@@ -128,6 +126,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
}
ngOnDestroy() {
this.dispose();
if (this.notebookService) {
this.notebookService.removeNotebookEditor(this);
}
@@ -138,7 +137,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
}
public get activeCellId(): string {
return this._activeCellId;
return this._model && this._model.activeCell ? this._model.activeCell.id : '';
}
public get modelRegistered(): Promise<NotebookModel> {
@@ -158,32 +157,28 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
if (event) {
event.stopPropagation();
}
if (cell !== this._activeCell) {
if (this._activeCell) {
this._activeCell.active = false;
if (cell !== this.model.activeCell) {
if (this.model.activeCell) {
this.model.activeCell.active = false;
}
this._activeCell = cell;
this._activeCell.active = true;
this._model.activeCell = this._activeCell;
this._activeCellId = cell.id;
this._model.activeCell = cell;
this._model.activeCell.active = true;
this._changeRef.detectChanges();
}
}
public unselectActiveCell() {
if (this._activeCell) {
this._activeCell.active = false;
if (this.model.activeCell) {
this.model.activeCell.active = false;
}
this._activeCell = null;
this._model.activeCell = null;
this._activeCellId = null;
this._changeRef.detectChanges();
}
// Add cell based on cell type
public addCell(cellType: CellType)
{
this._model.addCell(cellType);
let newCell = this._model.addCell(cellType);
this.selectCell(newCell);
}
// Updates Notebook model's trust details
@@ -201,12 +196,12 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
switch (event.key) {
case 'ArrowDown':
case 'ArrowRight':
let nextIndex = (this.findCellIndex(this._activeCell) + 1) % this.cells.length;
let nextIndex = (this.findCellIndex(this.model.activeCell) + 1) % this.cells.length;
this.selectCell(this.cells[nextIndex]);
break;
case 'ArrowUp':
case 'ArrowLeft':
let index = this.findCellIndex(this._activeCell);
let index = this.findCellIndex(this.model.activeCell);
if (index === 0) {
index = this.cells.length;
}
@@ -236,22 +231,29 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
private async loadModel(): Promise<void> {
await this.awaitNonDefaultProvider();
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this._notebookParams.providerId, this._notebookParams.notebookUri);
for (let providerId of this._notebookParams.providers) {
let notebookManager = await this.notebookService.getOrCreateNotebookManager(providerId, this._notebookParams.notebookUri);
this.notebookManagers.push(notebookManager);
}
let model = new NotebookModel({
factory: this.modelFactory,
notebookUri: this._notebookParams.notebookUri,
connectionService: this.connectionManagementService,
notificationService: this.notificationService,
notebookManager: this.notebookManager
notebookManagers: this.notebookManagers,
providerId: notebookUtils.sqlNotebooksEnabled() ? 'sql' : 'jupyter' // this is tricky; really should also depend on the connection profile
}, false, this.profile);
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
await model.requestModelLoad(this._notebookParams.isTrusted);
model.contentChanged((change) => this.handleContentChanged(change));
this._model = model;
this._model = this._register(model);
this.updateToolbarComponents(this._model.trustedMode);
this._register(model);
this._modelRegisteredDeferred.resolve(this._model);
model.backgroundStartSession();
// Set first cell as default active cell
if (this._model && this._model.cells && this._model.cells[0]) {
this.selectCell(model.cells[0]);
}
this._changeRef.detectChanges();
}
@@ -260,7 +262,9 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
await this.notebookService.registrationComplete;
// Refresh the provider if we had been using default
if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) {
this._notebookParams.providerId = notebookUtils.getProviderForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService);
let providers= notebookUtils.getProvidersForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService);
let tsqlProvider = providers.find(provider => provider === SQL_NOTEBOOK_PROVIDER);
this._notebookParams.providerId = tsqlProvider ? SQL_NOTEBOOK_PROVIDER : providers[0];
}
if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) {
// If it's still the default, warn them they should install an extension

View File

@@ -88,6 +88,7 @@ export class NotebookEditor extends BaseEditor {
notebookUri: input.notebookUri,
input: input,
providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER,
providers: input.providers ? input.providers : [DEFAULT_NOTEBOOK_PROVIDER],
isTrusted: input.isTrusted
};
bootstrapAngular(this.instantiationService,

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