From a0c30517bd46a67d95b393e9e4352e4122bc2a73 Mon Sep 17 00:00:00 2001 From: Gene Lee Date: Mon, 1 Oct 2018 16:58:22 -0700 Subject: [PATCH 01/26] Fix for broken checkbox status in checkbox tree node (#2705) --- src/sql/parts/modelComponents/tree/tree.component.ts | 7 ++++--- .../parts/modelComponents/tree/treeComponentRenderer.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sql/parts/modelComponents/tree/tree.component.ts b/src/sql/parts/modelComponents/tree/tree.component.ts index 973a0cc866..421cfb8369 100644 --- a/src/sql/parts/modelComponents/tree/tree.component.ts +++ b/src/sql/parts/modelComponents/tree/tree.component.ts @@ -82,9 +82,10 @@ export default class TreeComponent extends ComponentBase implements IComponent, } public refreshDataProvider(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeComponentItem }): void { - // if (this._dataProvider) { - // const itemsToRefresh = this._dataProvider.getItemsToRefresh(itemsToRefreshByHandle); - // } + if (this._dataProvider) { + this._dataProvider.getItemsToRefresh(itemsToRefreshByHandle); + } + if (this._tree) { for (const item of Object.values(itemsToRefreshByHandle)) { this._tree.refresh(item); diff --git a/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts b/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts index 854bcba356..6923471116 100644 --- a/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts +++ b/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts @@ -142,7 +142,7 @@ export class TreeComponentRenderer extends Disposable implements IRenderer { if (element) { element.onCheckedChanged = (checked: boolean) => { this._dataProvider.onNodeCheckedChanged(element.handle, checked); - } + }; templateData.model = element; } if (templateId === TreeComponentRenderer.DEFAULT_TEMPLATE) { From 1df7f25cada6756c5669435b1eb768fe6f8e6192 Mon Sep 17 00:00:00 2001 From: kenvanhyning Date: Tue, 2 Oct 2018 11:25:01 -0700 Subject: [PATCH 02/26] add sql class for task icon images (#2736) --- src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts b/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts index ecd20d52e9..bbc47701fa 100644 --- a/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts +++ b/src/sql/parts/taskHistory/viewlet/taskHistoryRenderer.ts @@ -19,7 +19,7 @@ import * as Utils from 'sql/parts/connection/common/utils'; export class TaskHistoryRenderer implements IRenderer { public static readonly TASKOBJECT_HEIGHT = 65; - private static readonly ICON_CLASS = 'task-icon icon'; + private static readonly ICON_CLASS = 'task-icon sql icon'; private static readonly TASKOBJECT_TEMPLATE_ID = 'carbonTask'; private static readonly FAIL_CLASS = 'error'; private static readonly SUCCESS_CLASS = 'success'; From 3f84e8e652805ba3117d8a4fccb9f46b8ec63264 Mon Sep 17 00:00:00 2001 From: Dinakar Nethi Date: Tue, 2 Oct 2018 17:27:32 -0700 Subject: [PATCH 03/26] Updated formatting changes (#2747) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91d0c4cf5e..a179a1dcd6 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Localization is now opened for 10 languages: French, Italian, German, Spanish, S ## Privacy Statement The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. -## Contributions and "thank you" +## 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: * AlexFsmn `Feature: Ability to add connection name #2332` @@ -87,7 +87,7 @@ We would like to thank all our users who raised issues, and in particular the fo * SebastianPfliegel `Remove sqlExtensionHelp (#312)` * olljanat for `Implemented npm version check (#314)` * Adam Mechanic for helping with the `whoisactive` extension -* All community localization contributors +* All community localization contributors: * French: Adrien Clerbois, ANAS BELABBES, Antoine Griffard, Arian Papillon, Eric Macarez, Eric Van Thorre, Jérémy LANDON, Matthias GROSPERRIN, Maxime COQUEREL, Olivier Guinart, thierry DEMAN-BARCELÒ, Thomas Potier * Italian: Aldo Donetti, Alessandro Alpi, Andrea Dottor, Bruni Luca, Gianluca Hotz, Luca Nardi, Luigi Bruno, Marco Dal Pino, Mirco Vanini, Pasquale Ceglie, Riccardo Cappello, Sergio Govoni, Stefano Demiliani * German: Anna Henke-Gunvaldson, Ben Weissman, David Ullmer, J.M. ., Kai Modo, Konstantin Staschill, Kostja Klein, Lennart Trunk, Markus Ehrenmüller-Jensen, Mascha Kroenlein, Matthias Knoll, Mourad Louha, Thomas Hütter, Wolfgang Straßer From a77bb50b9edc6cfd4c7e83bb02ddb58dd8548af0 Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Wed, 3 Oct 2018 10:41:07 -0700 Subject: [PATCH 04/26] Port the Azure Resource Explorer extension to core. (#2701) * Port the Azure Resource Explorer extension to core. This will enable Azure viewlet by default in the next release. - Moving this code from the SQL Server 2019 extension to Azure Data Studio core - Ported tests and verified they work in the integration tests.sh file - Fixed an issue that caused integration tests to fail if you have a SQL Server 2019 big data cluster endpoint listed, but the extension isn't installed. --- extensions/azurecore/.vscodeignore | 0 extensions/azurecore/README.md | 21 + extensions/azurecore/package.json | 128 ++++ extensions/azurecore/package.nls.json | 13 + extensions/azurecore/resources/azure.svg | 6 + .../resources/dark/account_inverse.svg | 1 + .../resources/dark/connect_to_inverse.svg | 1 + .../resources/dark/filter_inverse.svg | 1 + .../resources/dark/folder_inverse.svg | 1 + .../resources/dark/refresh_inverse.svg | 1 + .../resources/dark/sql_database_inverse.svg | 1 + .../resources/dark/sql_server_inverse.svg | 1 + .../resources/dark/subscription_inverse.svg | 1 + .../azurecore/resources/light/account.svg | 1 + .../azurecore/resources/light/connect_to.svg | 1 + .../azurecore/resources/light/filter.svg | 1 + .../azurecore/resources/light/folder.svg | 1 + .../azurecore/resources/light/refresh.svg | 1 + .../resources/light/sql_database.svg | 1 + .../azurecore/resources/light/sql_server.svg | 1 + .../resources/light/subscription.svg | 1 + extensions/azurecore/src/apiWrapper.ts | 225 +++++++ extensions/azurecore/src/appContext.ts | 28 + .../azurecore/src/azureResource/commands.ts | 120 ++++ .../azurecore/src/azureResource/constants.ts | 16 + .../azurecore/src/azureResource/errors.ts | 15 + .../azurecore/src/azureResource/interfaces.ts | 54 ++ .../azurecore/src/azureResource/models.ts | 25 + .../src/azureResource/servicePool.ts | 35 + .../azureResource/services/accountService.ts | 32 + .../azureResource/services/cacheService.ts | 25 + .../azureResource/services/contextService.ts | 36 ++ .../services/credentialService.ts | 43 ++ .../services/databaseServerService.ts | 39 ++ .../azureResource/services/databaseService.ts | 51 ++ .../services/subscriptionFilterService.ts | 77 +++ .../services/subscriptionService.ts | 33 + .../tree/accountNotSignedInTreeNode.ts | 51 ++ .../src/azureResource/tree/accountTreeNode.ts | 155 +++++ .../src/azureResource/tree/baseTreeNodes.ts | 71 ++ .../tree/databaseContainerTreeNode.ts | 103 +++ .../tree/databaseServerContainerTreeNode.ts | 103 +++ .../tree/databaseServerTreeNode.ts | 57 ++ .../azureResource/tree/databaseTreeNode.ts | 61 ++ .../src/azureResource/tree/messageTreeNode.ts | 60 ++ .../tree/subscriptionTreeNode.ts | 65 ++ .../azureResource/tree/treeChangeHandler.ts | 12 + .../src/azureResource/tree/treeProvider.ts | 101 +++ .../azurecore/src/azureResource/utils.ts | 44 ++ extensions/azurecore/src/constants.ts | 8 + .../src/controllers/controllerBase.ts | 34 + .../src/controllers/mainController.ts | 54 ++ extensions/azurecore/src/extension.ts | 40 ++ .../tree/accountNotSignedInTreeNode.test.ts | 37 ++ .../tree/accountTreeNode.test.ts | 294 +++++++++ .../tree/databaseContainerTreeNode.test.ts | 219 +++++++ .../databaseServerContainerTreeNode.test.ts | 219 +++++++ .../tree/databaseServerTreeNode.test.ts | 62 ++ .../tree/databaseTreeNode.test.ts | 62 ++ .../tree/messageTreeNode.test.ts | 41 ++ .../tree/subscriptionTreeNode.test.ts | 91 +++ .../azureResource/tree/treeProvider.test.ts | 113 ++++ extensions/azurecore/src/test/index.ts | 30 + extensions/azurecore/src/treeNodes.ts | 77 +++ extensions/azurecore/src/typings/ref.d.ts | 9 + extensions/azurecore/tsconfig.json | 22 + extensions/azurecore/yarn.lock | 608 ++++++++++++++++++ scripts/test-integration.bat | 5 +- scripts/test-integration.sh | 4 +- .../common/connectionManagementService.ts | 10 +- 70 files changed, 3955 insertions(+), 5 deletions(-) create mode 100644 extensions/azurecore/.vscodeignore create mode 100644 extensions/azurecore/README.md create mode 100644 extensions/azurecore/package.json create mode 100644 extensions/azurecore/package.nls.json create mode 100644 extensions/azurecore/resources/azure.svg create mode 100644 extensions/azurecore/resources/dark/account_inverse.svg create mode 100644 extensions/azurecore/resources/dark/connect_to_inverse.svg create mode 100644 extensions/azurecore/resources/dark/filter_inverse.svg create mode 100644 extensions/azurecore/resources/dark/folder_inverse.svg create mode 100644 extensions/azurecore/resources/dark/refresh_inverse.svg create mode 100644 extensions/azurecore/resources/dark/sql_database_inverse.svg create mode 100644 extensions/azurecore/resources/dark/sql_server_inverse.svg create mode 100644 extensions/azurecore/resources/dark/subscription_inverse.svg create mode 100644 extensions/azurecore/resources/light/account.svg create mode 100644 extensions/azurecore/resources/light/connect_to.svg create mode 100644 extensions/azurecore/resources/light/filter.svg create mode 100644 extensions/azurecore/resources/light/folder.svg create mode 100644 extensions/azurecore/resources/light/refresh.svg create mode 100644 extensions/azurecore/resources/light/sql_database.svg create mode 100644 extensions/azurecore/resources/light/sql_server.svg create mode 100644 extensions/azurecore/resources/light/subscription.svg create mode 100644 extensions/azurecore/src/apiWrapper.ts create mode 100644 extensions/azurecore/src/appContext.ts create mode 100644 extensions/azurecore/src/azureResource/commands.ts create mode 100644 extensions/azurecore/src/azureResource/constants.ts create mode 100644 extensions/azurecore/src/azureResource/errors.ts create mode 100644 extensions/azurecore/src/azureResource/interfaces.ts create mode 100644 extensions/azurecore/src/azureResource/models.ts create mode 100644 extensions/azurecore/src/azureResource/servicePool.ts create mode 100644 extensions/azurecore/src/azureResource/services/accountService.ts create mode 100644 extensions/azurecore/src/azureResource/services/cacheService.ts create mode 100644 extensions/azurecore/src/azureResource/services/contextService.ts create mode 100644 extensions/azurecore/src/azureResource/services/credentialService.ts create mode 100644 extensions/azurecore/src/azureResource/services/databaseServerService.ts create mode 100644 extensions/azurecore/src/azureResource/services/databaseService.ts create mode 100644 extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts create mode 100644 extensions/azurecore/src/azureResource/services/subscriptionService.ts create mode 100644 extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/accountTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/messageTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts create mode 100644 extensions/azurecore/src/azureResource/tree/treeProvider.ts create mode 100644 extensions/azurecore/src/azureResource/utils.ts create mode 100644 extensions/azurecore/src/constants.ts create mode 100644 extensions/azurecore/src/controllers/controllerBase.ts create mode 100644 extensions/azurecore/src/controllers/mainController.ts create mode 100644 extensions/azurecore/src/extension.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts create mode 100644 extensions/azurecore/src/test/index.ts create mode 100644 extensions/azurecore/src/treeNodes.ts create mode 100644 extensions/azurecore/src/typings/ref.d.ts create mode 100644 extensions/azurecore/tsconfig.json create mode 100644 extensions/azurecore/yarn.lock diff --git a/extensions/azurecore/.vscodeignore b/extensions/azurecore/.vscodeignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/azurecore/README.md b/extensions/azurecore/README.md new file mode 100644 index 0000000000..003e776621 --- /dev/null +++ b/extensions/azurecore/README.md @@ -0,0 +1,21 @@ +# Azure (Core) extension for Azure Data Studio + +Welcome to the Azure (Core) extension for Azure Data Studio! This extension supports core Azure functionality such as browsing and connecting to Azure data endpoints. In the current release the following features are supported: + +* Log in to Azure and browse your accounts, subscriptions and data resources +* See Azure SQL Databases and Servers in the tree, and open these connections in Object Explorer +* Filter the list of subscriptions for a given account, to make finding specific databases easier + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Privacy Statement + +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt). diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json new file mode 100644 index 0000000000..d490feffbb --- /dev/null +++ b/extensions/azurecore/package.json @@ -0,0 +1,128 @@ +{ + "name": "azurecore", + "displayName": "%azure.displayName%", + "description": "%azure.description", + "version": "0.1.0", + "publisher": "Microsoft", + "preview": true, + "engines": { + "vscode": "^1.25.0", + "sqlops": "*" + }, + "activationEvents": [ + "onView:azureResourceExplorer" + ], + "main": "./out/extension", + "contributes": { + "configuration": [ + { + "type": "object", + "title": "%azure.config.title%", + "properties": { + "azureResource.resourceFilter": { + "type": "array", + "default": null, + "description": "%azure.resourceFilter.description%" + } + } + } + ], + "commands": [ + { + "command": "azureresource.refreshall", + "title": "%azureresource.refreshall%", + "icon": { + "dark": "resources/dark/refresh_inverse.svg", + "light": "resources/light/refresh.svg" + } + }, + { + "command": "azureresource.refresh", + "title": "%azureresource.refresh%", + "icon": { + "dark": "resources/dark/refresh_inverse.svg", + "light": "resources/light/refresh.svg" + } + }, + { + "command": "azureresource.signin", + "title": "%azureresource.signin%" + }, + { + "command": "azureresource.connectsqldb", + "title": "%azureresource.connectsqldb%", + "icon": { + "dark": "resources/dark/connect_to_inverse.svg", + "light": "resources/light/connect_to.svg" + } + }, + { + "command": "azureresource.selectsubscriptions", + "title": "%azureresource.selectsubscriptions%", + "icon": { + "dark": "resources/dark/filter_inverse.svg", + "light": "resources/light/filter.svg" + } + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "azureResource", + "title": "%azure.title%", + "icon": "resources/azure.svg" + } + ] + }, + "views": { + "azureResource": [ + { + "id": "azureResourceExplorer", + "name": "%azure.resourceExplorer.title%" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "azureresource.refreshall", + "when": "view == azureResourceExplorer", + "group": "navigation@1" + } + ], + "view/item/context": [ + { + "command": "azureresource.connectsqldb", + "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", + "group": "1azureresource@1" + }, + { + "command": "azureresource.connectsqldb", + "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", + "group": "inline" + }, + { + "command": "azureresource.selectsubscriptions", + "when": "viewItem == azureResource.itemType.account", + "group": "inline" + }, + { + "command": "azureresource.refresh", + "when": "viewItem != azureResource.itemType.database && viewItem != azureResource.itemType.databaseServer && viewItem != azureResource.itemType.message", + "group": "inline" + } + ] + } + }, + "dependencies": { + "azure-arm-resource": "^7.0.0", + "azure-arm-sql": "^5.0.1", + "vscode-nls": "^4.0.0" + }, + "devDependencies": { + "@types/mocha": "^5.2.5", + "mocha": "^5.2.0", + "should": "^13.2.1", + "typemoq": "^2.1.0" + } +} diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json new file mode 100644 index 0000000000..5b3abedb0b --- /dev/null +++ b/extensions/azurecore/package.nls.json @@ -0,0 +1,13 @@ +{ + "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" +} \ No newline at end of file diff --git a/extensions/azurecore/resources/azure.svg b/extensions/azurecore/resources/azure.svg new file mode 100644 index 0000000000..dd17231563 --- /dev/null +++ b/extensions/azurecore/resources/azure.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/extensions/azurecore/resources/dark/account_inverse.svg b/extensions/azurecore/resources/dark/account_inverse.svg new file mode 100644 index 0000000000..0429cb4941 --- /dev/null +++ b/extensions/azurecore/resources/dark/account_inverse.svg @@ -0,0 +1 @@ +account_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/connect_to_inverse.svg b/extensions/azurecore/resources/dark/connect_to_inverse.svg new file mode 100644 index 0000000000..9d796bdc76 --- /dev/null +++ b/extensions/azurecore/resources/dark/connect_to_inverse.svg @@ -0,0 +1 @@ +connect_to_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/filter_inverse.svg b/extensions/azurecore/resources/dark/filter_inverse.svg new file mode 100644 index 0000000000..60b90abd6d --- /dev/null +++ b/extensions/azurecore/resources/dark/filter_inverse.svg @@ -0,0 +1 @@ +filter_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/folder_inverse.svg b/extensions/azurecore/resources/dark/folder_inverse.svg new file mode 100644 index 0000000000..f94d427cb1 --- /dev/null +++ b/extensions/azurecore/resources/dark/folder_inverse.svg @@ -0,0 +1 @@ +folder_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/refresh_inverse.svg b/extensions/azurecore/resources/dark/refresh_inverse.svg new file mode 100644 index 0000000000..d79fdaa4e8 --- /dev/null +++ b/extensions/azurecore/resources/dark/refresh_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_database_inverse.svg b/extensions/azurecore/resources/dark/sql_database_inverse.svg new file mode 100644 index 0000000000..5eaaf7e5f0 --- /dev/null +++ b/extensions/azurecore/resources/dark/sql_database_inverse.svg @@ -0,0 +1 @@ +sql_database_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_server_inverse.svg b/extensions/azurecore/resources/dark/sql_server_inverse.svg new file mode 100644 index 0000000000..1ce3f48b39 --- /dev/null +++ b/extensions/azurecore/resources/dark/sql_server_inverse.svg @@ -0,0 +1 @@ +sql_server_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/subscription_inverse.svg b/extensions/azurecore/resources/dark/subscription_inverse.svg new file mode 100644 index 0000000000..491fb5e895 --- /dev/null +++ b/extensions/azurecore/resources/dark/subscription_inverse.svg @@ -0,0 +1 @@ +subscription_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/light/account.svg b/extensions/azurecore/resources/light/account.svg new file mode 100644 index 0000000000..91de1865c0 --- /dev/null +++ b/extensions/azurecore/resources/light/account.svg @@ -0,0 +1 @@ +account \ No newline at end of file diff --git a/extensions/azurecore/resources/light/connect_to.svg b/extensions/azurecore/resources/light/connect_to.svg new file mode 100644 index 0000000000..1f420533d6 --- /dev/null +++ b/extensions/azurecore/resources/light/connect_to.svg @@ -0,0 +1 @@ +connect_to \ No newline at end of file diff --git a/extensions/azurecore/resources/light/filter.svg b/extensions/azurecore/resources/light/filter.svg new file mode 100644 index 0000000000..32f914f54a --- /dev/null +++ b/extensions/azurecore/resources/light/filter.svg @@ -0,0 +1 @@ +filter_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/folder.svg b/extensions/azurecore/resources/light/folder.svg new file mode 100644 index 0000000000..517c9b185d --- /dev/null +++ b/extensions/azurecore/resources/light/folder.svg @@ -0,0 +1 @@ +folder_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/refresh.svg b/extensions/azurecore/resources/light/refresh.svg new file mode 100644 index 0000000000..e034574819 --- /dev/null +++ b/extensions/azurecore/resources/light/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_database.svg b/extensions/azurecore/resources/light/sql_database.svg new file mode 100644 index 0000000000..e0c1584d98 --- /dev/null +++ b/extensions/azurecore/resources/light/sql_database.svg @@ -0,0 +1 @@ +sql_database \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_server.svg b/extensions/azurecore/resources/light/sql_server.svg new file mode 100644 index 0000000000..03cff4d932 --- /dev/null +++ b/extensions/azurecore/resources/light/sql_server.svg @@ -0,0 +1 @@ +sql_server \ No newline at end of file diff --git a/extensions/azurecore/resources/light/subscription.svg b/extensions/azurecore/resources/light/subscription.svg new file mode 100644 index 0000000000..5fda717712 --- /dev/null +++ b/extensions/azurecore/resources/light/subscription.svg @@ -0,0 +1 @@ +subscription \ No newline at end of file diff --git a/extensions/azurecore/src/apiWrapper.ts b/extensions/azurecore/src/apiWrapper.ts new file mode 100644 index 0000000000..7a13231e26 --- /dev/null +++ b/extensions/azurecore/src/apiWrapper.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as sqlops from 'sqlops'; + +import * as constants from './constants'; + +/** + * Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into + * this API from our code + * + * @export + * @class ApiWrapper + */ +export class ApiWrapper { + // Data APIs + public registerConnectionProvider(provider: sqlops.ConnectionProvider): vscode.Disposable { + return sqlops.dataprotocol.registerConnectionProvider(provider); + } + + public registerObjectExplorerProvider(provider: sqlops.ObjectExplorerProvider): vscode.Disposable { + return sqlops.dataprotocol.registerObjectExplorerProvider(provider); + } + + public registerTaskServicesProvider(provider: sqlops.TaskServicesProvider): vscode.Disposable { + return sqlops.dataprotocol.registerTaskServicesProvider(provider); + } + + public registerFileBrowserProvider(provider: sqlops.FileBrowserProvider): vscode.Disposable { + return sqlops.dataprotocol.registerFileBrowserProvider(provider); + } + + public registerCapabilitiesServiceProvider(provider: sqlops.CapabilitiesProvider): vscode.Disposable { + return sqlops.dataprotocol.registerCapabilitiesServiceProvider(provider); + } + + public registerModelViewProvider(widgetId: string, handler: (modelView: sqlops.ModelView) => void): void { + return sqlops.ui.registerModelViewProvider(widgetId, handler); + } + + public registerWebviewProvider(widgetId: string, handler: (webview: sqlops.DashboardWebview) => void): void { + return sqlops.dashboard.registerWebviewProvider(widgetId, handler); + } + + public createDialog(title: string): sqlops.window.modelviewdialog.Dialog { + return sqlops.window.modelviewdialog.createDialog(title); + } + + public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + return sqlops.window.modelviewdialog.openDialog(dialog); + } + + public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + return sqlops.window.modelviewdialog.closeDialog(dialog); + } + + public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void { + sqlops.tasks.registerTask(taskId, handler); + } + + public startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void { + sqlops.tasks.startBackgroundOperation(operationInfo); + } + + public getActiveConnections(): Thenable { + return sqlops.connection.getActiveConnections(); + } + + public getCurrentConnection(): Thenable { + return sqlops.connection.getCurrentConnection(); + } + + public createModelViewEditor(title: string, options?: sqlops.ModelViewEditorOptions): sqlops.workspace.ModelViewEditor { + return sqlops.workspace.createModelViewEditor(title, options); + } + + // VSCode APIs + public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { + return vscode.window.createTerminal(name, shellPath, shellArgs); + } + + public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { + return vscode.window.createTerminal(options); + } + + public executeCommand(command: string, ...rest: any[]): Thenable { + return vscode.commands.executeCommand(command, ...rest); + } + + public getFilePathRelativeToWorkspace(uri: vscode.Uri): string { + return vscode.workspace.asRelativePath(uri); + } + + public getWorkspaceFolders(): vscode.WorkspaceFolder[] { + return vscode.workspace.workspaceFolders; + } + + public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined { + let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { + return vscode.commands.registerCommand(command, callback, thisArg); + } + + public registerDocumentOpenHandler(handler: (doc: vscode.TextDocument) => any): vscode.Disposable { + return vscode.workspace.onDidOpenTextDocument(handler); + } + + public registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { + return vscode.window.registerTreeDataProvider(viewId, treeDataProvider); + } + + public setCommandContext(key: string, value: any): Thenable { + return vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, key, value); + } + + /** + * Get the configuration for a extensionName + * @param extensionName The string name of the extension to get the configuration for + * @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations + */ + public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration { + if (typeof resource === 'string') { + try { + resource = this.parseUri(resource); + } catch (e) { + resource = undefined; + } + } + return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri); + } + + public getExtensionConfiguration(): vscode.WorkspaceConfiguration { + return this.getConfiguration(constants.extensionConfigSectionName); + } + + /** + * Parse uri + */ + public parseUri(uri: string): vscode.Uri { + return vscode.Uri.parse(uri); + } + + public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { + return vscode.window.showOpenDialog(options); + } + + public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { + return vscode.window.showSaveDialog(options); + } + + public openTextDocument(uri: vscode.Uri): Thenable; + public openTextDocument(options: { language?: string; content?: string; }): Thenable; + public openTextDocument(uriOrOptions): Thenable { + return vscode.workspace.openTextDocument(uriOrOptions); + } + + public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean, preview?: boolean): Thenable { + let options: vscode.TextDocumentShowOptions = { + viewColumn: column, + preserveFocus: preserveFocus, + preview: preview + }; + return vscode.window.showTextDocument(document, options); + } + + public showErrorMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showErrorMessage(message, ...items); + } + + public showWarningMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showWarningMessage(message, ...items); + } + + public showInformationMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showInformationMessage(message, ...items); + } + + public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { + return vscode.window.createStatusBarItem(alignment, priority); + } + + public get workspaceFolders(): vscode.WorkspaceFolder[] { + return vscode.workspace.workspaceFolders; + } + + public createOutputChannel(name: string): vscode.OutputChannel { + return vscode.window.createOutputChannel(name); + } + + public createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage { + return sqlops.window.modelviewdialog.createWizardPage(title); + } + + public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { + return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters); + } + + public createTab(title: string): sqlops.window.modelviewdialog.DialogTab { + return sqlops.window.modelviewdialog.createTab(title); + } + + // Account APIs + public getAllAccounts(): Thenable { + return sqlops.accounts.getAllAccounts(); + } + + public getSecurityToken(account: sqlops.Account): Thenable<{}> { + return sqlops.accounts.getSecurityToken(account); + } + + public readonly onDidChangeAccounts = sqlops.accounts.onDidChangeAccounts; + + // Connection APIs + public openConnectionDialog(providers: string[], initialConnectionProfile?: sqlops.IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Thenable { + return sqlops.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions); + } +} diff --git a/extensions/azurecore/src/appContext.ts b/extensions/azurecore/src/appContext.ts new file mode 100644 index 0000000000..4abec27439 --- /dev/null +++ b/extensions/azurecore/src/appContext.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import { ApiWrapper } from './apiWrapper'; + +/** + * Global context for the application + */ +export class AppContext { + + private serviceMap: Map = new Map(); + constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) { + this.apiWrapper = apiWrapper || new ApiWrapper(); + } + + public getService(serviceName: string): T { + return this.serviceMap.get(serviceName) as T; + } + + public registerService(serviceName: string, service: T): void { + this.serviceMap.set(serviceName, service); + } +} diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts new file mode 100644 index 0000000000..0edb925c6f --- /dev/null +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { window, QuickPickItem } from 'vscode'; +import { IConnectionProfile } from 'sqlops'; +import { generateGuid } from './utils'; +import { ApiWrapper } from '../apiWrapper'; +import { TreeNode } from '../treeNodes'; + +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'; + +export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: AzureResourceTreeProvider): void { + apiWrapper.registerCommand('azureresource.selectsubscriptions', async (node?: TreeNode) => { + if (!(node instanceof AzureResourceAccountTreeNode)) { + return; + } + + const accountNode = node as AzureResourceAccountTreeNode; + + const servicePool = AzureResourceServicePool.getInstance(); + + let subscriptions = await accountNode.getCachedSubscriptions(); + if (!subscriptions || subscriptions.length === 0) { + const credentials = await servicePool.credentialService.getCredentials(accountNode.account); + subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials); + } + + const selectedSubscriptions = (await servicePool.subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || []; + const selectedSubscriptionIds: string[] = []; + if (selectedSubscriptions.length > 0) { + selectedSubscriptionIds.push(...selectedSubscriptions.map((subscription) => subscription.id)); + } else { + // ALL subscriptions are selected by default + selectedSubscriptionIds.push(...subscriptions.map((subscription) => subscription.id)); + } + + interface SubscriptionQuickPickItem extends QuickPickItem { + subscription: AzureResourceSubscription; + } + + const subscriptionItems: SubscriptionQuickPickItem[] = subscriptions.map((subscription) => { + return { + label: subscription.name, + picked: selectedSubscriptionIds.indexOf(subscription.id) !== -1, + subscription: subscription + }; + }); + + const pickedSubscriptionItems = (await window.showQuickPick(subscriptionItems, { canPickMany: true })); + if (pickedSubscriptionItems && pickedSubscriptionItems.length > 0) { + tree.refresh(node, false); + + const pickedSubscriptions = pickedSubscriptionItems.map((subscriptionItem) => subscriptionItem.subscription); + await servicePool.subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, pickedSubscriptions); + } + }); + + apiWrapper.registerCommand('azureresource.refreshall', () => tree.notifyNodeChanged(undefined)); + + apiWrapper.registerCommand('azureresource.refresh', async (node?: TreeNode) => { + tree.refresh(node, true); + }); + + apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => { + let connectionProfile: 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'); + }); +} diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts new file mode 100644 index 0000000000..3fb5368927 --- /dev/null +++ b/extensions/azurecore/src/azureResource/constants.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +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' +} diff --git a/extensions/azurecore/src/azureResource/errors.ts b/extensions/azurecore/src/azureResource/errors.ts new file mode 100644 index 0000000000..c0fe4a842f --- /dev/null +++ b/extensions/azurecore/src/azureResource/errors.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * 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 class AzureResourceCredentialError extends Error { + constructor( + message: string, + public innerError: Error + ) { + super(message); + } +} diff --git a/extensions/azurecore/src/azureResource/interfaces.ts b/extensions/azurecore/src/azureResource/interfaces.ts new file mode 100644 index 0000000000..5d21c5d408 --- /dev/null +++ b/extensions/azurecore/src/azureResource/interfaces.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account, DidChangeAccountsParams } from 'sqlops'; +import { Event } from 'vscode'; + +import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models'; + +export interface IAzureResourceAccountService { + getAccounts(): Promise; + + readonly onDidChangeAccounts: Event; +} + +export interface IAzureResourceCredentialService { + getCredentials(account: Account): Promise; +} + +export interface IAzureResourceSubscriptionService { + getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceSubscriptionFilterService { + getSelectedSubscriptions(account: Account): Promise; + + saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise; +} + +export interface IAzureResourceDatabaseServerService { + getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceDatabaseService { + getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceCacheService { + get(key: string): T | undefined; + + update(key: string, value: T): void; +} + +export interface IAzureResourceContextService { + getAbsolutePath(relativePath: string): string; + + executeCommand(commandId: string, ...args: any[]): void; + + showErrorMessage(errorMessage: string): void; +} diff --git a/extensions/azurecore/src/azureResource/models.ts b/extensions/azurecore/src/azureResource/models.ts new file mode 100644 index 0000000000..21e5b4ab37 --- /dev/null +++ b/extensions/azurecore/src/azureResource/models.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 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; +} diff --git a/extensions/azurecore/src/azureResource/servicePool.ts b/extensions/azurecore/src/azureResource/servicePool.ts new file mode 100644 index 0000000000..27aff58d98 --- /dev/null +++ b/extensions/azurecore/src/azureResource/servicePool.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { + 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(); +} diff --git a/extensions/azurecore/src/azureResource/services/accountService.ts b/extensions/azurecore/src/azureResource/services/accountService.ts new file mode 100644 index 0000000000..bc38f823a5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/accountService.ts @@ -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 { Event } from 'vscode'; +import { Account, DidChangeAccountsParams } from 'sqlops'; +import { ApiWrapper } from '../../apiWrapper'; + +import { IAzureResourceAccountService } from '../interfaces'; + +export class AzureResourceAccountService implements IAzureResourceAccountService { + public constructor( + apiWrapper: ApiWrapper + ) { + this._apiWrapper = apiWrapper; + this._onDidChangeAccounts = this._apiWrapper.onDidChangeAccounts; + } + + public async getAccounts(): Promise { + return await this._apiWrapper.getAllAccounts(); + } + + public get onDidChangeAccounts(): Event { + return this._onDidChangeAccounts; + } + + private _apiWrapper: ApiWrapper = undefined; + private _onDidChangeAccounts: Event = undefined; +} diff --git a/extensions/azurecore/src/azureResource/services/cacheService.ts b/extensions/azurecore/src/azureResource/services/cacheService.ts new file mode 100644 index 0000000000..ba0d100273 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/cacheService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IAzureResourceCacheService } from "../interfaces"; + +export class AzureResourceCacheService implements IAzureResourceCacheService { + public constructor( + public readonly context: ExtensionContext + ) { + } + + public get(key: string): T | undefined { + return this.context.workspaceState.get(key); + } + + public update(key: string, value: T): void { + this.context.workspaceState.update(key, value); + } +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/services/contextService.ts b/extensions/azurecore/src/azureResource/services/contextService.ts new file mode 100644 index 0000000000..0e153d48e5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/contextService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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; +} diff --git a/extensions/azurecore/src/azureResource/services/credentialService.ts b/extensions/azurecore/src/azureResource/services/credentialService.ts new file mode 100644 index 0000000000..028181fb87 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/credentialService.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } 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: Account): Promise { + try { + let credentials: TokenCredentials[] = []; + let tokens = await this._apiWrapper.getSecurityToken(account); + + 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; +} diff --git a/extensions/azurecore/src/azureResource/services/databaseServerService.ts b/extensions/azurecore/src/azureResource/services/databaseServerService.ts new file mode 100644 index 0000000000..9145629a34 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/databaseServerService.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'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 { + 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; + } +} diff --git a/extensions/azurecore/src/azureResource/services/databaseService.ts b/extensions/azurecore/src/azureResource/services/databaseService.ts new file mode 100644 index 0000000000..643db5c1e6 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/databaseService.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + 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; + } +} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts new file mode 100644 index 0000000000..9e6bea1b08 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { WorkspaceConfiguration, ConfigurationTarget } from 'vscode'; +import { Account } from 'sqlops'; + +import { IAzureResourceSubscriptionFilterService, IAzureResourceCacheService } from '../interfaces'; +import { AzureResourceSubscription } from '../models'; + +interface AzureResourceSelectedSubscriptionsCache { + selectedSubscriptions: { [accountId: string]: AzureResourceSubscription[]}; +} + +export class AzureResourceSubscriptionFilterService implements IAzureResourceSubscriptionFilterService { + public constructor( + cacheService: IAzureResourceCacheService + ) { + this._cacheService = cacheService; + } + + public async getSelectedSubscriptions(account: Account): Promise { + let selectedSubscriptions: AzureResourceSubscription[] = []; + + const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + if (cache) { + selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId]; + } + + return selectedSubscriptions; + } + + public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise { + let selectedSubscriptionsCache: { [accountId: string]: AzureResourceSubscription[]} = {}; + + const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + if (cache) { + selectedSubscriptionsCache = cache.selectedSubscriptions; + } + + if (!selectedSubscriptionsCache) { + selectedSubscriptionsCache = {}; + } + + selectedSubscriptionsCache[account.key.accountId] = selectedSubscriptions; + + this._cacheService.update(AzureResourceSubscriptionFilterService.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(AzureResourceSubscriptionFilterService.FilterConfigName); + let configTarget = ConfigurationTarget.Global; + if (resourceFilterConfig) { + if (resourceFilterConfig.workspaceFolderValue) { + configTarget = ConfigurationTarget.WorkspaceFolder; + } else if (resourceFilterConfig.workspaceValue) { + configTarget = ConfigurationTarget.Workspace; + } else if (resourceFilterConfig.globalValue) { + configTarget = ConfigurationTarget.Global; + } + } + + await this._config.update(AzureResourceSubscriptionFilterService.FilterConfigName, filters, configTarget); + } + + private _config: WorkspaceConfiguration = undefined; + private _cacheService: IAzureResourceCacheService = undefined; + + private static readonly FilterConfigName = 'resourceFilter'; + private static readonly CacheKey = 'azureResource.cache.selectedSubscriptions'; +} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionService.ts b/extensions/azurecore/src/azureResource/services/subscriptionService.ts new file mode 100644 index 0000000000..9a8cfe0779 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/subscriptionService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } from 'sqlops'; +import { ServiceClientCredentials } from 'ms-rest'; +import { SubscriptionClient } from 'azure-arm-resource'; + +import { IAzureResourceSubscriptionService } from '../interfaces'; +import { AzureResourceSubscription } from '../models'; + +export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService { + public async getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise { + 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. + } + } + + return subscriptions; + } +} diff --git a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts new file mode 100644 index 0000000000..47008428aa --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceItemType } from '../constants'; + +export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { + public getChildren(): TreeNode[] | Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.SignInLabel, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.message; + item.command = { + title: AzureResourceAccountNotSignedInTreeNode.SignInLabel, + command: 'azureresource.signin', + arguments: [this] + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: AzureResourceAccountNotSignedInTreeNode.SignInLabel, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.message, + nodeSubType: undefined, + iconType: AzureResourceItemType.message + }; + } + + public get nodePathValue(): string { + return 'message_accountNotSignedIn'; + } + + private static readonly SignInLabel = localize('azureResource.tree.accountNotSignedInTreeNode.signIn', 'Sign in to Azure ...'); +} diff --git a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts new file mode 100644 index 0000000000..a95a485ece --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceErrorMessageUtil } from '../utils'; +import { AzureResourceSubscription } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeProvider'; + +export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase { + public constructor( + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler + ) { + super(account, treeChangeHandler, undefined); + + this._id = `account_${this.account.key.accountId}`; + this._label = this.generateLabel(); + } + + public async getChildren(): Promise { + try { + let subscriptions: AzureResourceSubscription[] = []; + + if (this._isClearingCache) { + const credentials = await this.getCredentials(); + subscriptions = (await this.servicePool.subscriptionService.getSubscriptions(this.account, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { subscriptions: { } }; + } + cache.subscriptions[this.account.key.accountId] = subscriptions; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + subscriptions = await this.getCachedSubscriptions(); + } + + this._totalSubscriptionCount = subscriptions.length; + + let selectedSubscriptions = await this.servicePool.subscriptionFilterService.getSelectedSubscriptions(this.account); + let selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id); + if (selectedSubscriptionIds.length > 0) { + subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1); + this._selectedSubscriptionCount = selectedSubscriptionIds.length; + } else { + // ALL subscriptions are listed by default + this._selectedSubscriptionCount = this._totalSubscriptionCount; + } + + this.refreshLabel(); + + if (subscriptions.length === 0) { + return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.NoSubscriptions, this)]; + } else { + return subscriptions.map((subscription) => new AzureResourceSubscriptionTreeNode(subscription, this.account, this.treeChangeHandler, this)); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } + } + + public async getCachedSubscriptions(): Promise { + const subscriptions: AzureResourceSubscription[] = []; + const cache = this.getCache(); + if (cache) { + subscriptions.push(...cache.subscriptions[this.account.key.accountId]); + } + return subscriptions; + } + + public getTreeItem(): TreeItem | Promise { + let 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') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this._label, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.account, + nodeSubType: undefined, + iconType: AzureResourceItemType.account + }; + } + + public get nodePathValue(): string { + return this._id; + } + + public get totalSubscriptionCount(): number { + return this._totalSubscriptionCount; + } + + public get selectedSubscriptionCount(): number { + return this._selectedSubscriptionCount; + } + + protected refreshLabel(): void { + const newLabel = this.generateLabel(); + if (this._label !== newLabel) { + this._label = newLabel; + this.treeChangeHandler.notifyNodeChanged(this); + } + } + + protected get cacheKey(): string { + return 'azureResource.cache.subscriptions'; + } + + private generateLabel(): string { + let label = `${this.account.displayInfo.displayName} (${this.account.key.accountId})`; + + if (this._totalSubscriptionCount !== 0) { + label += ` (${this._selectedSubscriptionCount} / ${this._totalSubscriptionCount} subscriptions)`; + } + + return label; + } + + 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[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts new file mode 100644 index 0000000000..ecea876ec1 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } from 'sqlops'; +import { ServiceClientCredentials } from 'ms-rest'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceServicePool } from '../servicePool'; +import { AzureResourceCredentialError } from '../errors'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; + +export abstract class AzureResourceTreeNodeBase extends TreeNode { + public constructor( + public readonly treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(); + + this.parent = parent; + } + + public readonly servicePool = AzureResourceServicePool.getInstance(); +} + +export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase { + public constructor( + public readonly account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + } + + public clearCache(): void { + this._isClearingCache = true; + } + + public get isClearingCache(): boolean { + return this._isClearingCache; + } + + protected async getCredentials(): Promise { + try { + return await this.servicePool.credentialService.getCredentials(this.account); + } catch (error) { + if (error instanceof AzureResourceCredentialError) { + this.servicePool.contextService.showErrorMessage(error.message); + + this.servicePool.contextService.executeCommand('azureresource.signin'); + } else { + throw error; + } + } + } + + protected updateCache(cache: T): void { + this.servicePool.cacheService.update(this.cacheKey, cache); + } + + protected getCache(): T { + return this.servicePool.cacheService.get(this.cacheKey); + } + + protected abstract get cacheKey(): string; + + protected _isClearingCache = true; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts new file mode 100644 index 0000000000..26d5f6d70c --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + try { + let databases: AzureResourceDatabase[] = []; + + if (this._isClearingCache) { + let credentials = await this.getCredentials(); + databases = (await this.servicePool.databaseService.getDatabases(this.subscription, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { databases: { } }; + } + cache.databases[this.subscription.id] = databases; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + const cache = this.getCache(); + if (cache) { + databases = cache.databases[this.subscription.id] || []; + } + } + + 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 { + 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[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts new file mode 100644 index 0000000000..6f2921e092 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + try { + let databaseServers: AzureResourceDatabaseServer[] = []; + + if (this._isClearingCache) { + let credentials = await this.getCredentials(); + databaseServers = (await this.servicePool.databaseServerService.getDatabaseServers(this.subscription, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { databaseServers: { } }; + } + cache.databaseServers[this.subscription.id] = databaseServers; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + const cache = this.getCache(); + if (cache) { + databaseServers = cache.databaseServers[this.subscription.id] || []; + } + } + + 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 { + 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[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts new file mode 100644 index 0000000000..2c8cf5bd4b --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'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 { + return []; + } + + public getTreeItem(): TreeItem | Promise { + 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}`; + } +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts new file mode 100644 index 0000000000..1089a6e0c5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + return []; + } + + public getTreeItem(): TreeItem | Promise { + 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; +} diff --git a/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts b/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts new file mode 100644 index 0000000000..edea2e5a76 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceItemType } from '../constants'; + +export class AzureResourceMessageTreeNode extends TreeNode { + public constructor( + public readonly message: string, + parent: TreeNode + ) { + super(); + + this.parent = parent; + this._id = `message_${AzureResourceMessageTreeNode._messageNum++}`; + } + + public static create(message: string, parent: TreeNode): AzureResourceMessageTreeNode { + return new AzureResourceMessageTreeNode(message, parent); + } + + public getChildren(): TreeNode[] | Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(this.message, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.message; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this.message, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.message, + nodeSubType: undefined, + iconType: AzureResourceItemType.message + }; + } + + public get nodePathValue(): string { + return this._id; + } + + private _id: string; + + private static _messageNum: number = 0; +} diff --git a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts new file mode 100644 index 0000000000..ff8474ca3f --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Account, NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceTreeNodeBase, AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceDatabaseContainerTreeNode } from './databaseContainerTreeNode'; +import { AzureResourceDatabaseServerContainerTreeNode } from './databaseServerContainerTreeNode'; +import { AzureResourceSubscription } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; + +export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase { + public constructor( + public readonly subscription: AzureResourceSubscription, + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + + this._children.push(new AzureResourceDatabaseContainerTreeNode(subscription, account, treeChangeHandler, this)); + this._children.push(new AzureResourceDatabaseServerContainerTreeNode(subscription, account, treeChangeHandler, this)); + } + + public async getChildren(): Promise { + return this._children; + } + + public getTreeItem(): TreeItem | Promise { + let 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') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this.subscription.name, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.subscription, + nodeSubType: undefined, + iconType: AzureResourceItemType.subscription + }; + } + + public get nodePathValue(): string { + return `subscription_${this.subscription.id}`; + } + + private _children: AzureResourceContainerTreeNodeBase[] = []; +} diff --git a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts new file mode 100644 index 0000000000..b7db2ed6b4 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeNode } from '../../treeNodes'; + +export interface IAzureResourceTreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts new file mode 100644 index 0000000000..7cfd4ce89f --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/treeProvider.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeDataProvider, EventEmitter, Event, TreeItem } from 'vscode'; +import { DidChangeAccountsParams } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import { setInterval, clearInterval } from 'timers'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceServicePool } from '../servicePool'; +import { AzureResourceAccountTreeNode } from './accountTreeNode'; +import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceContainerTreeNodeBase, AzureResourceTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceErrorMessageUtil } from '../utils'; + +export interface IAzureResourceTreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} + +export class AzureResourceTreeProvider implements TreeDataProvider, IAzureResourceTreeChangeHandler { + public constructor() { + AzureResourceServicePool.getInstance().accountService.onDidChangeAccounts((e: DidChangeAccountsParams) => { this._onDidChangeTreeData.fire(undefined); }); + } + + public async getChildren(element?: TreeNode): Promise { + if (element) { + return element.getChildren(true); + } + + if (!this.isSystemInitialized) { + this._loadingTimer = setInterval(async () => { + try { + // Call sqlops.accounts.getAllAccounts() to determine whether the system has been initialized. + await AzureResourceServicePool.getInstance().accountService.getAccounts(); + + // System has been initialized + this.isSystemInitialized = true; + + if (this._loadingTimer) { + clearInterval(this._loadingTimer); + } + + this._onDidChangeTreeData.fire(undefined); + } catch (error) { + // System not initialized yet + this.isSystemInitialized = false; + } + }, AzureResourceTreeProvider.LoadingTimerInterval); + + return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.Loading, undefined)]; + } + + try { + const accounts = await AzureResourceServicePool.getInstance().accountService.getAccounts(); + + if (accounts && accounts.length > 0) { + return accounts.map((account) => new AzureResourceAccountTreeNode(account, this)); + } else { + return [new AzureResourceAccountNotSignedInTreeNode()]; + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), undefined)]; + } + } + + public get onDidChangeTreeData(): Event { + return this._onDidChangeTreeData.event; + } + + public notifyNodeChanged(node: TreeNode): void { + this._onDidChangeTreeData.fire(node); + } + + public async refresh(node: TreeNode, isClearingCache: boolean): Promise { + if (isClearingCache) { + if ((node instanceof AzureResourceContainerTreeNodeBase)) { + node.clearCache(); + } + } + + this._onDidChangeTreeData.fire(node); + } + + public getTreeItem(element: TreeNode): TreeItem | Thenable { + return element.getTreeItem(); + } + + public isSystemInitialized: boolean = false; + + private _loadingTimer: NodeJS.Timer = undefined; + private _onDidChangeTreeData = new EventEmitter(); + + private static readonly Loading = localize('azureResource.tree.treeProvider.loading', 'Loading ...'); + private static readonly LoadingTimerInterval = 5000; +} diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts new file mode 100644 index 0000000000..99f177eafb --- /dev/null +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +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)); + } +} + +export function generateGuid(): string { + let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) + let oct: string = ''; + let tmp: number; + /* tslint:disable:no-bitwise */ + for (let a: number = 0; a < 4; a++) { + tmp = (4294967296 * Math.random()) | 0; + oct += hexValues[tmp & 0xF] + + hexValues[tmp >> 4 & 0xF] + + hexValues[tmp >> 8 & 0xF] + + hexValues[tmp >> 12 & 0xF] + + hexValues[tmp >> 16 & 0xF] + + hexValues[tmp >> 20 & 0xF] + + hexValues[tmp >> 24 & 0xF] + + hexValues[tmp >> 28 & 0xF]; + } + + // 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively' + let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0]; + return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12); + /* tslint:enable:no-bitwise */ +} \ No newline at end of file diff --git a/extensions/azurecore/src/constants.ts b/extensions/azurecore/src/constants.ts new file mode 100644 index 0000000000..994cfc1f6a --- /dev/null +++ b/extensions/azurecore/src/constants.ts @@ -0,0 +1,8 @@ +'use strict'; + +export const extensionConfigSectionName = 'azure'; +export const ViewType = 'view'; + +export enum BuiltInCommands { + SetContext = 'setContext' +} diff --git a/extensions/azurecore/src/controllers/controllerBase.ts b/extensions/azurecore/src/controllers/controllerBase.ts new file mode 100644 index 0000000000..15e31972b4 --- /dev/null +++ b/extensions/azurecore/src/controllers/controllerBase.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; + +import { AppContext } from '../appContext'; +import { ApiWrapper } from '../apiWrapper'; + +export default abstract class ControllerBase implements vscode.Disposable { + + public constructor(protected appContext: AppContext) { + } + + protected get apiWrapper(): ApiWrapper { + return this.appContext.apiWrapper; + } + + public get extensionContext(): vscode.ExtensionContext { + return this.appContext && this.appContext.extensionContext; + } + + abstract activate(): Promise; + + abstract deactivate(): void; + + public dispose(): void { + this.deactivate(); + } +} + diff --git a/extensions/azurecore/src/controllers/mainController.ts b/extensions/azurecore/src/controllers/mainController.ts new file mode 100644 index 0000000000..7f38fd8667 --- /dev/null +++ b/extensions/azurecore/src/controllers/mainController.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + 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); + } +} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts new file mode 100644 index 0000000000..9d693b3fec --- /dev/null +++ b/extensions/azurecore/src/extension.ts @@ -0,0 +1,40 @@ +'use strict'; + +import * as vscode from 'vscode'; + +import MainController from './controllers/mainController'; +import { AppContext } from './appContext'; +import ControllerBase from './controllers/controllerBase'; +import { ApiWrapper } from './apiWrapper'; + +let controllers: ControllerBase[] = []; + +// 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()); + let activations: Promise[] = []; + + // Start the main controller + let mainController = new MainController(appContext); + controllers.push(mainController); + extensionContext.subscriptions.push(mainController); + activations.push(mainController.activate()); + + return Promise.all(activations) + .then((results: boolean[]) => { + for (let result of results) { + if (!result) { + return false; + } + } + return true; + }); +} + +// this method is called when your extension is deactivated +export function deactivate() { + for (let controller of controllers) { + controller.deactivate(); + } +} diff --git a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts new file mode 100644 index 0000000000..9a3cb3f866 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts @@ -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 * as should from 'should'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceAccountNotSignedInTreeNode } from '../../../azureResource/tree/accountNotSignedInTreeNode'; + +describe('AzureResourceAccountNotSignedInTreeNode.info', function(): void { + it('Should be correct.', async function(): Promise { + const label = 'Sign in to Azure ...'; + + const treeNode = new AzureResourceAccountNotSignedInTreeNode(); + + should(treeNode.nodePathValue).equal('message_accountNotSignedIn'); + + const treeItem = await treeNode.getTreeItem(); + should(treeItem.label).equal(label); + should(treeItem.contextValue).equal(AzureResourceItemType.message); + 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'); + + const nodeInfo = treeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(label); + should(nodeInfo.nodeType).equal(AzureResourceItemType.message); + should(nodeInfo.iconType).equal(AzureResourceItemType.message); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts new file mode 100644 index 0000000000..27b64e0160 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * 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, + IAzureResourceSubscriptionService, + IAzureResourceSubscriptionFilterService +} 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'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockCacheService: TypeMoq.IMock; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockSubscriptionService: TypeMoq.IMock; +let mockSubscriptionFilterService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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().object; +const mockCredentials = [mockCredential]; + +const mockSubscription1: AzureResourceSubscription = { + id: 'mock_subscription_1', + name: 'mock subscription 1' +}; +const mockSubscription2: AzureResourceSubscription = { + id: 'mock_subscription_2', + name: 'mock subscription 2' +}; +const mockSubscriptions = [mockSubscription1, mockSubscription2]; +const mockFilteredSubscriptions = [mockSubscription1]; + +let mockSubscriptionCache: { subscriptions: { [accountId: string]: AzureResourceSubscription[]} }; + +describe('AzureResourceAccountTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockSubscriptionCache = { subscriptions: {} }; + + mockServicePool.contextService = mockContextService.object; + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.subscriptionService = mockSubscriptionService.object; + mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + }); + + it('Should be correct when created.', async function(): Promise { + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const accountTreeNodeId = `account_${mockAccount.key.accountId}`; + const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId})`; + + should(accountTreeNode.nodePathValue).equal(accountTreeNodeId); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.id).equal(accountTreeNodeId); + should(treeItem.label).equal(accountTreeNodeLabel); + should(treeItem.contextValue).equal(AzureResourceItemType.account); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + should(nodeInfo.isLeaf).false(); + should(nodeInfo.nodeType).equal(AzureResourceItemType.account); + should(nodeInfo.iconType).equal(AzureResourceItemType.account); + }); + + it('Should be correct when there are subscriptions listed.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).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); + + await accountTreeNode.getChildren(); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.label).equal(accountTreeNodeLabel); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + }); + + it('Should be correct when there are subscriptions filtered.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).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); + + await accountTreeNode.getChildren(); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.label).equal(accountTreeNodeLabel); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + }); +}); + +describe('AzureResourceAccountTreeNode.getChildren', function(): void { + beforeEach(() => { + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockSubscriptionCache = { subscriptions: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.subscriptionService = mockSubscriptionService.object; + mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + }); + + it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); + + mockTreeChangeHandler.verify((o) => o.notifyNodeChanged(accountTreeNode), TypeMoq.Times.once()); + + should(accountTreeNode.totalSubscriptionCount).equal(mockSubscriptions.length); + should(accountTreeNode.selectedSubscriptionCount).equal(mockSubscriptions.length); + should(accountTreeNode.isClearingCache).false(); + + 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); + + 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}`); + } + }); + + it('Should load subscriptions from cache when it is not clearing cache.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + await accountTreeNode.getChildren(); + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), 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)); + + should(children.length).equal(mockSubscriptionCache.subscriptions[mockAccount.key.accountId].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}`); + } + }); + + it('Should handle when there is no subscriptions.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + should(accountTreeNode.totalSubscriptionCount).equal(0); + + 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 Subscriptions found.'); + }); + + it('Should honor subscription filtering.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); + + should(accountTreeNode.selectedSubscriptionCount).equal(mockFilteredSubscriptions.length); + should(children.length).equal(mockFilteredSubscriptions.length); + + for (let ix = 0; ix < mockFilteredSubscriptions.length; ix++) { + should(children[ix].nodePathValue).equal(`subscription_${mockFilteredSubscriptions[ix].id}`); + } + }); + + it('Should handle errors.', async function(): Promise { + const mockError = 'Test error'; + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => { throw new Error(mockError); }); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), 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('AzureResourceAccountTreeNode.clearCache', function() : void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should clear cache.', async function(): Promise { + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + accountTreeNode.clearCache(); + should(accountTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts new file mode 100644 index 0000000000..a2e8640265 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * 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; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockDatabaseService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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().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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + 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(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockDatabaseService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockDatabaseContainerCache = { databases: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.databaseService = mockDatabaseService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).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 { + 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), 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 { + 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), 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 { + 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 { + 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), 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(); + }); + + it('Should clear cache.', async function(): Promise { + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + databaseContainerTreeNode.clearCache(); + should(databaseContainerTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts new file mode 100644 index 0000000000..5bbed2f812 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * 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; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockDatabaseServerService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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().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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + 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(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockDatabaseServerService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockDatabaseServerContainerCache = { databaseServers: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.databaseServerService = mockDatabaseServerService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).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 { + 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), 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 { + 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), 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 { + 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 { + 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), 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(); + }); + + it('Should clear cache.', async function(): Promise { + const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + databaseServerContainerTreeNode.clearCache(); + should(databaseServerContainerTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts new file mode 100644 index 0000000000..cef163b670 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + 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); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts new file mode 100644 index 0000000000..a1350771b8 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct.', async function(): Promise { + 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); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts new file mode 100644 index 0000000000..ddd867c60f --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts @@ -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 * as should from 'should'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; + +describe('AzureResourceMessageTreeNode.info', function(): void { + it('Should be correct when created.', async function(): Promise { + const mockMessage = 'Test messagse'; + const treeNode = new AzureResourceMessageTreeNode(mockMessage, undefined); + + should(treeNode.nodePathValue).startWith('message_'); + + const treeItem = await treeNode.getTreeItem(); + should(treeItem.label).equal(mockMessage); + should(treeItem.contextValue).equal(AzureResourceItemType.message); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + + const nodeInfo = treeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(mockMessage); + should(nodeInfo.nodeType).equal(AzureResourceItemType.message); + should(nodeInfo.iconType).equal(AzureResourceItemType.message); + }); +}); + +describe('AzureResourceMessageTreeNode.create', function(): void { + it('Should create a message node.', async function(): Promise { + const mockMessage = 'Test messagse'; + const treeNode = AzureResourceMessageTreeNode.create(mockMessage, undefined); + should(treeNode).instanceof(AzureResourceMessageTreeNode); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts new file mode 100644 index 0000000000..e55621dfed --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { IAzureResourceContextService } from '../../../azureResource/interfaces'; +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'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockContextService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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: AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +describe('AzureResourceSubscriptionTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + should(subscriptionTreeNode.nodePathValue).equal(`subscription_${mockSubscription.id}`); + + const treeItem = await subscriptionTreeNode.getTreeItem(); + should(treeItem.label).equal(mockSubscription.name); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + should(treeItem.contextValue).equal(AzureResourceItemType.subscription); + + const nodeInfo = subscriptionTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(mockSubscription.name); + should(nodeInfo.isLeaf).equal(false); + should(nodeInfo.nodeType).equal(AzureResourceItemType.subscription); + should(nodeInfo.iconType).equal(AzureResourceItemType.subscription); + }); +}); + +describe('AzureResourceSubscriptionTreeNode.getChildren', function(): void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should load database containers.', async function(): Promise { + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + const children = await subscriptionTreeNode.getChildren(); + + should(children).Array(); + should(children.length).equal(2); + should(children[0]).instanceof(AzureResourceDatabaseContainerTreeNode); + should(children[1]).instanceof(AzureResourceDatabaseServerContainerTreeNode); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts new file mode 100644 index 0000000000..f8cf33c84c --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { 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'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockAccountService: TypeMoq.IMock; + +// Mock test data +const mockAccount1: sqlops.Account = { + key: { + accountId: 'mock_account_1', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account_1@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; +const mockAccount2: sqlops.Account = { + key: { + accountId: 'mock_account_2', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account_2@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; +const mockAccounts = [mockAccount1, mockAccount2]; + +describe('AzureResourceTreeProvider.getChildren', function(): void { + beforeEach(() => { + mockAccountService = TypeMoq.Mock.ofType(); + + mockServicePool.accountService = mockAccountService.object; + }); + + it('Should load accounts.', async function(): Promise { + mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(mockAccounts)); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); + + should(children).Array(); + should(children.length).equal(mockAccounts.length); + + for (let ix = 0; ix < mockAccounts.length; ix++) { + const child = children[ix]; + const account = mockAccounts[ix]; + + should(child).instanceof(AzureResourceAccountTreeNode); + should(child.nodePathValue).equal(`account_${account.key.accountId}`); + } + }); + + it('Should handle when there is no accounts.', async function(): Promise { + mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(undefined)); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceAccountNotSignedInTreeNode); + }); + + it('Should handle errors.', async function(): Promise { + const mockAccountError = 'Test account error'; + mockAccountService.setup((o) => o.getAccounts()).returns(() => { throw new Error(mockAccountError); }); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); + + 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: ${mockAccountError}`); + }); +}); diff --git a/extensions/azurecore/src/test/index.ts b/extensions/azurecore/src/test/index.ts new file mode 100644 index 0000000000..a9f0ffbd82 --- /dev/null +++ b/extensions/azurecore/src/test/index.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('vscode/lib/testrunner'); + +const suite = 'Integration Azure Tests'; + +const options: any = { + ui: 'bdd', + useColors: true, + timeout: 60000 +}; + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/azurecore/src/treeNodes.ts b/extensions/azurecore/src/treeNodes.ts new file mode 100644 index 0000000000..7136ba98bd --- /dev/null +++ b/extensions/azurecore/src/treeNodes.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as sqlops from 'sqlops'; +import * 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) { + path = this.parent.generateNodePath(); + } + path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue; + return path; + } + + public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise { + let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path; + let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath); + return TreeNode.findNode(this, condition, filter, true); + } + + public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise { + if (!node) { + return undefined; + } + + if (condition(node)) { + return node; + } + + let nodeInfo = node.getNodeInfo(); + if (nodeInfo.isLeaf) { + return undefined; + } + + // TODO support filtering by already expanded / not yet expanded + let children = await node.getChildren(false); + if (children) { + for (let child of children) { + if (filter && filter(child)) { + let childNode = await this.findNode(child, condition, filter, expandIfNeeded); + if (childNode) { + return childNode; + } + } + } + } + return undefined; + } + + /** + * The value to use for this node in the node path + */ + public abstract get nodePathValue(): string; + + abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise; + abstract getTreeItem(): vscode.TreeItem | Promise; + + abstract getNodeInfo(): sqlops.NodeInfo; +} diff --git a/extensions/azurecore/src/typings/ref.d.ts b/extensions/azurecore/src/typings/ref.d.ts new file mode 100644 index 0000000000..41e273db7f --- /dev/null +++ b/extensions/azurecore/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// \ No newline at end of file diff --git a/extensions/azurecore/tsconfig.json b/extensions/azurecore/tsconfig.json new file mode 100644 index 0000000000..b341a65dab --- /dev/null +++ b/extensions/azurecore/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", "es2015.promise" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock new file mode 100644 index 0000000000..8c2535a991 --- /dev/null +++ b/extensions/azurecore/yarn.lock @@ -0,0 +1,608 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/mocha@^5.2.5": + version "5.2.5" + resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" + +"@types/node@^8.0.47": + version "8.10.30" + resolved "https://registry.npmjs.org/@types/node/-/node-8.10.30.tgz#2c82cbed5f79d72280c131d2acffa88fbd8dd353" + +adal-node@^0.1.28: + version "0.1.28" + resolved "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485" + dependencies: + "@types/node" "^8.0.47" + async ">=0.6.0" + date-utils "*" + jws "3.x.x" + request ">= 2.52.0" + underscore ">= 1.3.1" + uuid "^3.1.0" + xmldom ">= 0.1.x" + xpath.js "~1.1.0" + +ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async@2.6.0: + version "2.6.0" + resolved "https://registry.npmjs.org/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + dependencies: + lodash "^4.14.0" + +async@>=0.6.0: + version "2.6.1" + resolved "https://registry.npmjs.org/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + dependencies: + lodash "^4.17.10" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + +azure-arm-resource@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/azure-arm-resource/-/azure-arm-resource-7.0.0.tgz#e76fe2195abe354b607346c2fa0f690544176294" + dependencies: + ms-rest "^2.3.3" + ms-rest-azure "^2.5.5" + +azure-arm-sql@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/azure-arm-sql/-/azure-arm-sql-5.0.1.tgz#75c0b115525d2270ab16122d47d0c666aca175d4" + dependencies: + ms-rest "^2.3.3" + ms-rest-azure "^2.5.5" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + dependencies: + tweetnacl "^0.14.3" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +combined-stream@1.0.6: + version "1.0.6" + resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + dependencies: + delayed-stream "~1.0.0" + +commander@2.15.1: + version "2.15.1" + resolved "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-utils@*: + version "1.2.21" + resolved "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +duplexer@^0.1.1: + version "0.1.1" + resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-buffer@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@3.x.x: + version "3.1.5" + resolved "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + +lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4: + version "4.17.11" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + +mime-db@~1.36.0: + version "1.36.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.20" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + dependencies: + mime-db "~1.36.0" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@0.5.1: + version "0.5.1" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +moment@^2.21.0, moment@^2.22.2: + version "2.22.2" + resolved "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + +ms-rest-azure@^2.5.5: + version "2.5.9" + resolved "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.5.9.tgz#8599943e349c91eb367d2d1dcb885017518dc712" + dependencies: + adal-node "^0.1.28" + async "2.6.0" + moment "^2.22.2" + ms-rest "^2.3.2" + request "^2.88.0" + uuid "^3.2.1" + +ms-rest@^2.3.2, ms-rest@^2.3.3: + version "2.3.7" + resolved "https://registry.npmjs.org/ms-rest/-/ms-rest-2.3.7.tgz#8bfc82fb91807643fcaa487c5fc9698cd18a018c" + dependencies: + duplexer "^0.1.1" + is-buffer "^1.1.6" + is-stream "^1.1.0" + moment "^2.21.0" + request "^2.88.0" + through "^2.3.8" + tunnel "0.0.5" + uuid "^3.2.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +postinstall-build@^5.0.1: + version "5.0.3" + resolved "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" + +psl@^1.1.24: + version "1.1.29" + resolved "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +"request@>= 2.52.0", request@^2.88.0: + version "2.88.0" + resolved "https://registry.npmjs.org/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +should-equal@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" + dependencies: + should-type "^1.4.0" + +should-format@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" + dependencies: + should-type "^1.3.0" + should-type-adaptors "^1.0.1" + +should-type-adaptors@^1.0.1: + version "1.1.0" + resolved "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" + dependencies: + should-type "^1.3.0" + should-util "^1.0.0" + +should-type@^1.3.0, should-type@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" + +should-util@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" + +should@^13.2.1: + version "13.2.3" + resolved "https://registry.npmjs.org/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" + dependencies: + should-equal "^2.0.0" + should-format "^3.0.3" + should-type "^1.4.0" + should-type-adaptors "^1.0.1" + should-util "^1.0.0" + +sshpk@^1.7.0: + version "1.14.2" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + safer-buffer "^2.0.2" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + +through@^2.3.8: + version "2.3.8" + resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tunnel@0.0.5: + version "0.0.5" + resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz#d1532254749ed36620fcd1010865495a1fa9d0ae" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typemoq@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" + dependencies: + circular-json "^0.3.1" + lodash "^4.17.4" + postinstall-build "^5.0.1" + +"underscore@>= 1.3.1": + version "1.9.1" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + +uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +"xmldom@>= 0.1.x": + version "0.1.27" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xpath.js@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 929b92b9bc..4e613f5592 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -6,8 +6,11 @@ pushd %~dp0\.. set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% :: Tests in the extension host -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% +:: TODO port over an re-enable API tests +:: 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% 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% . diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index a48257ad4f..fba91607f9 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -13,9 +13,11 @@ fi cd $ROOT # Tests in the extension host -./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 +# TODO port over an re-enable API tests +# ./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 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 . diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index 9b057aa729..41612e8fb2 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -1349,8 +1349,12 @@ export class ConnectionManagementService extends Disposable implements IConnecti * TODO this could be a map reduce operation */ public buildConnectionInfo(connectionString: string, provider: string): Thenable { - return this._providers.get(provider).onReady.then(e => { - return e.buildConnectionInfo(connectionString); - }); + let connectionProvider = this._providers.get(provider); + if (connectionProvider) { + return connectionProvider.onReady.then(e => { + return e.buildConnectionInfo(connectionString); + }); + } + return Promise.resolve(undefined); } } From 597f29e90a1d47101e6cfab612dc4b76365627ff Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Wed, 3 Oct 2018 13:13:21 -0700 Subject: [PATCH 05/26] Alanren/edit data improvement (#2748) * improvements for dirty indicator and tab switching scenario * rename the event * update the angular2-slickgrid version --- package.json | 2 +- .../views/editData/editData.component.html | 1 + .../grid/views/editData/editData.component.ts | 139 ++++++++++++------ yarn.lock | 6 +- 4 files changed, 103 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 228eb4976a..c79a535016 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@angular/upgrade": "~4.1.3", "@types/chart.js": "^2.7.31", "angular2-grid": "2.0.6", - "angular2-slickgrid": "github:Microsoft/angular2-slickgrid#1.4.4", + "angular2-slickgrid": "github:Microsoft/angular2-slickgrid#1.4.5", "applicationinsights": "0.18.0", "chart.js": "^2.6.0", "fast-plist": "0.1.2", diff --git a/src/sql/parts/grid/views/editData/editData.component.html b/src/sql/parts/grid/views/editData/editData.component.html index 38df6df546..106fbd2e82 100644 --- a/src/sql/parts/grid/views/editData/editData.component.html +++ b/src/sql/parts/grid/views/editData/editData.component.html @@ -23,6 +23,7 @@ (onContextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)" [isCellEditValid]="onIsCellEditValid" [overrideCellFn]="overrideCellFn" + [onBeforeAppendCell]="onBeforeAppendCell" [enableEditing]="true" class="boxCol content vertBox slickgrid"> diff --git a/src/sql/parts/grid/views/editData/editData.component.ts b/src/sql/parts/grid/views/editData/editData.component.ts index 22035694e3..5c0ea466bd 100644 --- a/src/sql/parts/grid/views/editData/editData.component.ts +++ b/src/sql/parts/grid/views/editData/editData.component.ts @@ -39,6 +39,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { EditUpdateCellResult } from 'sqlops'; export const EDITDATA_SELECTOR: string = 'editdata-component'; @Component({ @@ -50,7 +51,10 @@ export const EDITDATA_SELECTOR: string = 'editdata-component'; export class EditDataComponent extends GridParentComponent implements OnInit, OnDestroy { // CONSTANTS private scrollTimeOutTime = 200; - private windowSize = 50; + + // Optimized for the edit top 200 rows scenario, only need to retrieve the data once + // to make the scroll experience smoother + private windowSize = 200; // FIELDS // All datasets @@ -67,6 +71,7 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On private newRowVisible: boolean; private removingNewRow: boolean; private rowIdMappings: { [gridRowId: number]: number } = {}; + private dirtyCells: number[] = []; protected plugins = new Array>>(); // Edit Data functions @@ -76,6 +81,7 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On public onIsColumnEditable: (column: number) => boolean; public overrideCellFn: (rowNumber, columnId, value?, data?) => string; public loadDataFunction: (offset: number, count: number) => Promise<{}[]>; + public onBeforeAppendCell: (row: number, column: number) => string; private savedViewState: { gridSelections: Slick.Range[]; @@ -179,6 +185,18 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On return returnVal; }; + // This is the event slickgrid will raise in order to get the additional cell CSS classes for the cell + // Due to performance advantage we are using this event instead of the onViewportChanged event. + this.onBeforeAppendCell = (row: number, column: number): string => { + let cellClass = undefined; + if (this.isRowDirty(row) && column === 0) { + cellClass = ' dirtyCell '; + } else if (this.isCellDirty(row, column)) { + cellClass = ' dirtyRowHeader '; + } + return cellClass; + }; + // Setup a function for generating a promise to lookup result subsets this.loadDataFunction = (offset: number, count: number): Promise<{}[]> => { return new Promise<{}[]>((resolve, reject) => { @@ -204,17 +222,6 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On }, {})); } - // let rowIndex = offset; - // let gridData: IGridDataRow[] = result.subset.map(row => { - // self.idMapping[rowIndex] = row.id; - // rowIndex++; - // return { - // values: [{}].concat(row.cells.map(c => { - // return mixin({ ariaLabel: escape(c.displayValue) }, c); - // })), row: row.id - // }; - // }); - resolve(gridData); }); }); @@ -258,33 +265,18 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On return; } - let cellSelectTasks: Promise = Promise.resolve(); - - if (this.currentCell.isEditable && this.currentEditCellValue !== null && !this.removingNewRow) { - // We're exiting a read/write cell after having changed the value, update the cell value in the service - cellSelectTasks = cellSelectTasks.then(() => { - // Use the mapped row ID if we're on that row - let sessionRowId = self.rowIdMappings[self.currentCell.row] !== undefined - ? self.rowIdMappings[self.currentCell.row] - : self.currentCell.row; - - return self.dataService.updateCell(sessionRowId, self.currentCell.column - 1, self.currentEditCellValue) - .then( - result => { - // Cell update was successful, update the flags - self.currentEditCellValue = null; - self.setCellDirtyState(row, self.currentCell.column, result.cell.isDirty); - self.setRowDirtyState(row, result.isRowDirty); - return Promise.resolve(); - }, - error => { - // Cell update failed, jump back to the last cell we were on - self.focusCell(self.currentCell.row, self.currentCell.column, true); - return Promise.reject(null); - } - ); + let cellSelectTasks: Promise = this.submitCurrentCellChange( + (result: EditUpdateCellResult) => { + // Cell update was successful, update the flags + self.setCellDirtyState(row, self.currentCell.column, result.cell.isDirty); + self.setRowDirtyState(row, result.isRowDirty); + return Promise.resolve(); + }, + (error) => { + // Cell update failed, jump back to the last cell we were on + self.focusCell(self.currentCell.row, self.currentCell.column, true); + return Promise.reject(null); }); - } if (this.currentCell.row !== row) { // If we're currently adding a new row, only commit it if it has changes or the user is trying to add another new row @@ -474,11 +466,38 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On // do not refresh the whole dataset as it will move the focus away to the first row. // this.currentEditCellValue = null; + this.dirtyCells = []; this.dataSet.dataRows.resetWindowsAroundIndex(this.currentCell.row); } } } + private submitCurrentCellChange(resultHandler, errorHandler): Promise { + let self = this; + let updateCellPromise: Promise = Promise.resolve(); + + if (this.currentCell && this.currentCell.isEditable && this.currentEditCellValue !== null && !this.removingNewRow) { + // We're exiting a read/write cell after having changed the value, update the cell value in the service + updateCellPromise = updateCellPromise.then(() => { + // Use the mapped row ID if we're on that row + let sessionRowId = self.rowIdMappings[self.currentCell.row] !== undefined + ? self.rowIdMappings[self.currentCell.row] + : self.currentCell.row; + + return self.dataService.updateCell(sessionRowId, self.currentCell.column - 1, self.currentEditCellValue); + }).then( + result => { + self.currentEditCellValue = null; + return resultHandler(result); + }, + error => { + return errorHandler(error); + } + ); + } + return updateCellPromise; + } + // Checks if input row is our NULL new row private isNullRow(row: number): boolean { // Null row is always at index (totalRows - 1) @@ -492,6 +511,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On if (dirtyState) { // Change cell color $(grid.getCellNode(row, column)).addClass('dirtyCell').removeClass('selected'); + if (this.dirtyCells.indexOf(column) === -1) { + this.dirtyCells.push(column); + } } else { $(grid.getCellNode(row, column)).removeClass('dirtyCell'); } @@ -515,6 +537,7 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On let allRows = $($('.grid-canvas').children()); let allCells = $(allRows.children()); allCells.removeClass('dirtyCell').removeClass('dirtyRowHeader'); + this.dirtyCells = []; } // Adds an extra row to the end of slickgrid (just for rendering purposes) @@ -594,16 +617,30 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On } private saveViewState(): void { - let grid = this.slickgrids.toArray()[0] + let grid = this.slickgrids.toArray()[0]; + let self = this; if (grid) { let gridSelections = grid.getSelectedRanges(); - let viewport = ((grid as any)._grid.getCanvasNode() as HTMLElement).parentElement; - + let gridObject = grid as any; + let viewport = (gridObject._grid.getCanvasNode() as HTMLElement).parentElement; this.savedViewState = { gridSelections, scrollTop: viewport.scrollTop, scrollLeft: viewport.scrollLeft }; + + // Save the cell that is currently being edited. + // Note: This is only updating the data in tools service, not saving the change to database. + // This is added to fix the data inconsistency: the updated value is displayed but won't be saved to the database + // when committing the changes for the row. + if (this.currentCell.row !== undefined && this.currentCell.column !== undefined && this.currentCell.isEditable) { + gridObject._grid.getEditorLock().commitCurrentEdit(); + this.submitCurrentCellChange((result: EditUpdateCellResult) => { + self.setCellDirtyState(self.currentCell.row, self.currentCell.column, result.cell.isDirty); + }, (error: any) => { + self.notificationService.error(error); + }); + } } } @@ -614,6 +651,26 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On viewport.scrollLeft = this.savedViewState.scrollLeft; viewport.scrollTop = this.savedViewState.scrollTop; this.savedViewState = undefined; + + // This block of code is responsible for restoring the dirty state indicators if slickgrid decides not to re-render the dirty row + // Other scenarios will be taken care of by getAdditionalCssClassesForCell method when slickgrid needs to re-render the rows. + if (this.currentCell.row !== undefined) { + if (this.isRowDirty(this.currentCell.row)) { + this.setRowDirtyState(this.currentCell.row, true); + + this.dirtyCells.forEach(cell => { + this.setCellDirtyState(this.currentCell.row, cell, true); + }); + } + } } } + + private isRowDirty(row: number): boolean { + return this.currentCell.row === row && this.dirtyCells.length > 0; + } + + private isCellDirty(row: number, column: number): boolean { + return this.currentCell.row === row && this.dirtyCells.indexOf(column) !== -1; + } } diff --git a/yarn.lock b/yarn.lock index f372631195..4406bdfae4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -178,9 +178,9 @@ angular2-grid@2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/angular2-grid/-/angular2-grid-2.0.6.tgz#01fe225dc13b2822370b6c61f9a6913b3a26f989" -"angular2-slickgrid@github:Microsoft/angular2-slickgrid#1.4.4": - version "1.4.4" - resolved "https://codeload.github.com/Microsoft/angular2-slickgrid/tar.gz/5494e99f3ffdfd119e02bd2a26db5ba3c1e05c1d" +"angular2-slickgrid@github:Microsoft/angular2-slickgrid#1.4.5": + version "1.4.5" + resolved "https://codeload.github.com/Microsoft/angular2-slickgrid/tar.gz/4a4dd9333f6295c9539c2c3f9a945a204c9a6449" ansi-colors@^1.0.1: version "1.1.0" From 75d27837c26acba6897a6c794b7ed4569b901675 Mon Sep 17 00:00:00 2001 From: Phil Campbell Date: Thu, 4 Oct 2018 11:44:26 -0700 Subject: [PATCH 06/26] center the icon (#2760) --- resources/darwin/code.icns | Bin 43649 -> 40937 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/darwin/code.icns b/resources/darwin/code.icns index a7cc8d2c8855ddf31518eaffa32a6054645ce308..4c28a056075497e9e203de9bb65cbde3a228f9fd 100644 GIT binary patch literal 40937 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%0AL0a4)K}bOrrB$yaFjD0jw;Hy<7_U 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 Date: Thu, 4 Oct 2018 11:48:41 -0700 Subject: [PATCH 07/26] change how query plan is handled (#2735) --- src/sql/parts/query/editor/gridPanel.ts | 6 ------ src/sql/parts/query/editor/queryResultsView.ts | 5 +++++ src/sql/parts/query/execution/queryRunner.ts | 6 ++++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/sql/parts/query/editor/gridPanel.ts b/src/sql/parts/query/editor/gridPanel.ts index f083fccc6b..12bd059aa5 100644 --- a/src/sql/parts/query/editor/gridPanel.ts +++ b/src/sql/parts/query/editor/gridPanel.ts @@ -552,12 +552,6 @@ class GridTable extends Disposable implements IView { private loadData(offset: number, count: number): Thenable { return this.runner.getQueryRows(offset, count, this.resultSet.batchId, this.resultSet.id).then(response => { - if (this.runner.isQueryPlan) { - // it's a show plan response - if (response.resultSubset.rowCount === 1) { - this.instantiationService.createInstance(ShowQueryPlanAction).run(response.resultSubset.rows[0][0].displayValue); - } - } return response.resultSubset.rows.map(r => { let dataWithSchema = {}; // skip the first column since its a number column diff --git a/src/sql/parts/query/editor/queryResultsView.ts b/src/sql/parts/query/editor/queryResultsView.ts index 942846cd20..67eb615482 100644 --- a/src/sql/parts/query/editor/queryResultsView.ts +++ b/src/sql/parts/query/editor/queryResultsView.ts @@ -203,6 +203,11 @@ export class QueryResultsView { if (!this._panelView.contains(this.qpTab)) { this._panelView.pushTab(this.qpTab); } + } else if (queryRunner.isQueryPlan) { + let disp = queryRunner.onResultSet(() => { + this.showPlan(queryRunner.planXml); + disp.dispose(); + }); } if (this.input.state.activeTab) { this._panelView.showTab(this.input.state.activeTab); diff --git a/src/sql/parts/query/execution/queryRunner.ts b/src/sql/parts/query/execution/queryRunner.ts index 2c6fc45c8e..ba1530b5a2 100644 --- a/src/sql/parts/query/execution/queryRunner.ts +++ b/src/sql/parts/query/execution/queryRunner.ts @@ -72,6 +72,8 @@ export default class QueryRunner { private _isQueryPlan: boolean; public get isQueryPlan(): boolean { return this._isQueryPlan; } + private _planXml: string; + public get planXml(): string { return this._planXml; } private _onMessage = new Emitter(); private _debouncedMessage = debounceEvent(this._onMessage.event, (l, e) => { @@ -338,6 +340,10 @@ export default class QueryRunner { } else { batchSet = this.batchSets[resultSet.batchId]; } + // handle getting queryPlanxml if we need too + if (this.isQueryPlan) { + this.getQueryRows(0, 1, 0, 0).then(e => this._planXml = e.resultSubset.rows[0][0].displayValue); + } if (batchSet) { // Store the result set in the batch and emit that a result set has completed batchSet.resultSetSummaries[resultSet.id] = resultSet; From b097b54792de2b8fa86902a69eac6b71a8d0ee77 Mon Sep 17 00:00:00 2001 From: Aditya Bist Date: Thu, 4 Oct 2018 13:52:25 -0700 Subject: [PATCH 08/26] Agent: Edit Job improvements (#2721) * fixed right click context menu bug in jobs view * added stepInfo and edit job WIP * show jobs in job edit * added schedule description on select schedule * fetch schedules during history and show in edit job * added alerts to job histories and show in edit * made history calls async * filter menus now close when esc is pressed * fixed bug where clicking on error row wouldnt populate job details --- extensions/agent/src/data/jobData.ts | 5 +- extensions/agent/src/dialogs/jobDialog.ts | 96 ++++++++++++++-- .../agent/src/dialogs/pickScheduleDialog.ts | 18 ++- .../ui/table/plugins/headerFilter.plugin.ts | 14 ++- .../common/jobManagementUtilities.ts | 4 +- .../views/jobHistory.component.ts | 4 +- .../views/jobStepsView.component.ts | 4 +- .../jobManagement/views/jobStepsViewTree.ts | 4 +- .../jobManagement/views/jobsView.component.ts | 107 +++++++++++++++--- src/sql/sqlops.d.ts | 4 + 10 files changed, 217 insertions(+), 43 deletions(-) diff --git a/extensions/agent/src/data/jobData.ts b/extensions/agent/src/data/jobData.ts index c5b6bff3c0..ca16a07208 100644 --- a/extensions/agent/src/data/jobData.ts +++ b/extensions/agent/src/data/jobData.ts @@ -59,6 +59,9 @@ export class JobData implements IAgentDialogData { this.category = jobInfo.category; this.description = jobInfo.description; this.enabled = jobInfo.enabled; + this.jobSteps = jobInfo.JobSteps; + this.jobSchedules = jobInfo.JobSchedules; + this.alerts = jobInfo.Alerts; } } @@ -105,8 +108,6 @@ export class JobData implements IAgentDialogData { displayName: this.JobCompletionActionCondition_Always, name: sqlops.JobCompletionActionCondition.Always.toString() }]; - - this.jobSchedules = []; } public async save() { diff --git a/extensions/agent/src/dialogs/jobDialog.ts b/extensions/agent/src/dialogs/jobDialog.ts index 926642ca15..ab86a4f699 100644 --- a/extensions/agent/src/dialogs/jobDialog.ts +++ b/extensions/agent/src/dialogs/jobDialog.ts @@ -62,6 +62,8 @@ export class JobDialog extends AgentDialog { private readonly AlertsTopLabelString: string = localize('jobDialog.alertsList', 'Alerts list'); private readonly NewAlertButtonString: string = localize('jobDialog.newAlert', 'New Alert'); private readonly AlertNameLabelString: string = localize('jobDialog.alertNameLabel', 'Alert Name'); + private readonly AlertEnabledLabelString: string = localize('jobDialog.alertEnabledLabel', 'Enabled'); + private readonly AlertTypeLabelString: string = localize('jobDialog.alertTypeLabel', 'Type'); // UI Components private generalTab: sqlops.window.modelviewdialog.DialogTab; @@ -197,6 +199,8 @@ export class JobDialog extends AgentDialog { .withProperties({ value: 'Feature Preview' }).component(); + let steps = this.model.jobSteps ? this.model.jobSteps : []; + let data = this.convertStepsToData(steps); this.stepsTable = view.modelBuilder.table() .withProperties({ columns: [ @@ -206,8 +210,8 @@ export class JobDialog extends AgentDialog { this.StepsTable_SuccessColumnString, this.StepsTable_FailureColumnString ], - data: [], - height: 430 + data: data, + height: 750 }).component(); this.moveStepUpButton = view.modelBuilder.button() @@ -250,6 +254,31 @@ export class JobDialog extends AgentDialog { }).component(); this.stepsTable.enabled = false; + this.editStepButton.enabled = false; + this.deleteStepButton.enabled = false; + + this.stepsTable.onRowSelected(() => { + // only let edit or delete steps if there's + // one step selection + if (this.stepsTable.selectedRows.length === 1) { + let rowNumber = this.stepsTable.selectedRows[0]; + let stepData = steps[rowNumber]; + this.deleteStepButton.enabled = true; + this.editStepButton.enabled = true; + this.editStepButton.onDidClick((e) => { + // implement edit steps + + // let stepDialog = new JobStepDialog(this.model.ownerUri, this.nameTextBox.value, '' , 1, this.model); + // stepDialog.openNewStepDialog(); + }); + + this.deleteStepButton.onDidClick((e) => { + // implement delete steps + + + }); + } + }); let formModel = view.modelBuilder.formContainer() .withFormItems([{ @@ -271,12 +300,16 @@ export class JobDialog extends AgentDialog { .withProperties({ value: 'Feature Preview' }).component(); + let alerts = this.model.alerts ? this.model.alerts : []; + let data = this.convertAlertsToData(alerts); this.alertsTable = view.modelBuilder.table() .withProperties({ columns: [ - this.AlertNameLabelString + this.AlertNameLabelString, + this.AlertEnabledLabelString, + this.AlertTypeLabelString ], - data: [], + data: data, height: 430, width: 400 }).component(); @@ -312,10 +345,12 @@ export class JobDialog extends AgentDialog { this.schedulesTable = view.modelBuilder.table() .withProperties({ columns: [ - this.ScheduleNameLabelString + PickScheduleDialog.SchedulesIDText, + PickScheduleDialog.ScheduleNameLabelText, + PickScheduleDialog.ScheduleDescription ], data: [], - height: 430, + height: 750, width: 420 }).component(); @@ -350,13 +385,11 @@ export class JobDialog extends AgentDialog { } private populateScheduleTable() { - if (this.model.jobSchedules) { - let data: any[][] = []; - for (let i = 0; i < this.model.jobSchedules.length; ++i) { - let schedule = this.model.jobSchedules[i]; - data[i] = [ schedule.name ]; - } + let schedules = this.model.jobSchedules ? this.model.jobSchedules : []; + let data = this.convertSchedulesToData(schedules); + if (data.length > 0) { this.schedulesTable.data = data; + this.schedulesTable.height = 750; } } @@ -466,6 +499,45 @@ export class JobDialog extends AgentDialog { }); } + private convertStepsToData(jobSteps: sqlops.AgentJobStepInfo[]): any[][] { + let result = []; + jobSteps.forEach(jobStep => { + let cols = []; + cols.push(jobStep.id); + cols.push(jobStep.stepName); + cols.push(jobStep.subSystem); + cols.push(jobStep.successAction); + cols.push(jobStep.failureAction); + result.push(cols); + }); + return result; + } + + private convertSchedulesToData(jobSchedules: sqlops.AgentJobScheduleInfo[]): any[][] { + let result = []; + jobSchedules.forEach(schedule => { + let cols = []; + cols.push(schedule.id); + cols.push(schedule.name); + cols.push(schedule.description); + result.push(cols); + }); + return result; + } + + private convertAlertsToData(alerts: sqlops.AgentAlertInfo[]): any[][] { + let result = []; + alerts.forEach(alert => { + let cols = []; + console.log(alert); + cols.push(alert.name); + cols.push(alert.isEnabled); + cols.push(alert.alertType.toString()); + result.push(cols); + }); + return result; + } + protected updateModel() { this.model.name = this.nameTextBox.value; this.model.owner = this.ownerTextBox.value; diff --git a/extensions/agent/src/dialogs/pickScheduleDialog.ts b/extensions/agent/src/dialogs/pickScheduleDialog.ts index d2d0a6188c..76e17e94a3 100644 --- a/extensions/agent/src/dialogs/pickScheduleDialog.ts +++ b/extensions/agent/src/dialogs/pickScheduleDialog.ts @@ -18,8 +18,11 @@ export class PickScheduleDialog { private readonly DialogTitle: string = localize('pickSchedule.jobSchedules', 'Job Schedules'); private readonly OkButtonText: string = localize('pickSchedule.ok', 'OK'); private readonly CancelButtonText: string = localize('pickSchedule.cancel', 'Cancel'); - private readonly ScheduleNameLabelText: string = localize('pickSchedule.scheduleName', 'Schedule Name'); - private readonly SchedulesLabelText: string = localize('pickSchedule.schedules', 'Schedules'); + private readonly SchedulesLabelText: string = localize('pickSchedule.availableSchedules', 'Available Schedules:'); + public static readonly ScheduleNameLabelText: string = localize('pickSchedule.scheduleName', 'Name'); + public static readonly SchedulesIDText: string = localize('pickSchedule.scheduleID','ID'); + public static readonly ScheduleDescription: string = localize('pickSchedule.description','Description'); + // UI Components private dialog: sqlops.window.modelviewdialog.Dialog; @@ -50,11 +53,13 @@ export class PickScheduleDialog { this.schedulesTable = view.modelBuilder.table() .withProperties({ columns: [ - this.ScheduleNameLabelText + PickScheduleDialog.SchedulesIDText, + PickScheduleDialog.ScheduleNameLabelText, + PickScheduleDialog.ScheduleDescription ], data: [], - height: '80em', - width: '40em' + height: 750, + width: 430 }).component(); let formModel = view.modelBuilder.formContainer() @@ -69,7 +74,8 @@ export class PickScheduleDialog { let data: any[][] = []; for (let i = 0; i < this.model.schedules.length; ++i) { let schedule = this.model.schedules[i]; - data[i] = [ schedule.name ]; + console.log(schedule); + data[i] = [ schedule.id, schedule.name, schedule.description ]; } this.schedulesTable.data = data; } diff --git a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts index 9a821b1ea6..a24cbaddab 100644 --- a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts @@ -39,16 +39,26 @@ export class HeaderFilter { this.handler.subscribe(this.grid.onHeaderCellRendered, (e, args) => this.handleHeaderCellRendered(e , args)) .subscribe(this.grid.onBeforeHeaderCellDestroy, (e, args) => this.handleBeforeHeaderCellDestroy(e, args)) .subscribe(this.grid.onClick, (e) => this.handleBodyMouseDown) - .subscribe(this.grid.onColumnsResized, () => this.columnsResized()); - + .subscribe(this.grid.onColumnsResized, () => this.columnsResized()) + .subscribe(this.grid.onKeyDown, (e) => this.handleKeyDown); this.grid.setColumns(this.grid.getColumns()); $(document.body).bind('mousedown', this.handleBodyMouseDown); + $(document.body).bind('keydown', this.handleKeyDown); } public destroy() { this.handler.unsubscribeAll(); $(document.body).unbind('mousedown', this.handleBodyMouseDown); + $(document.body).unbind('keydown', this.handleKeyDown); + } + + private handleKeyDown = (e) => { + if (this.$menu && (e.key === 'Escape' || e.keyCode === 27)) { + this.hideMenu(); + e.preventDefault(); + e.stopPropagation(); + } } private handleBodyMouseDown = (e) => { diff --git a/src/sql/parts/jobManagement/common/jobManagementUtilities.ts b/src/sql/parts/jobManagement/common/jobManagementUtilities.ts index febf1b4837..9b75625d20 100644 --- a/src/sql/parts/jobManagement/common/jobManagementUtilities.ts +++ b/src/sql/parts/jobManagement/common/jobManagementUtilities.ts @@ -17,7 +17,9 @@ export class JobManagementUtilities { switch(status) { case(0): return nls.localize('agentUtilities.failed','Failed'); case(1): return nls.localize('agentUtilities.succeeded', 'Succeeded'); - case(3): return nls.localize('agentUtilities.canceled', 'Canceled'); + case(2): return nls.localize('agentUtilities.retry', 'Retry'); + case(3): return nls.localize('agentUtilities.canceled', 'Cancelled'); + case(4): return nls.localize('agentUtilities.inProgress', 'In Progress'); case(5): return nls.localize('agentUtilities.statusUnknown', 'Status Unknown'); default: return nls.localize('agentUtilities.statusUnknown', 'Status Unknown'); } diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index 76148857ff..7bb4953634 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -185,8 +185,8 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { stepViewRow.runStatus = jobStepStatus ? JobManagementUtilities.convertToStatusString(0) : JobManagementUtilities.convertToStatusString(step.runStatus); self._runStatus = JobManagementUtilities.convertToStatusString(self.agentJobHistoryInfo.runStatus); - stepViewRow.stepName = step.stepName; - stepViewRow.stepID = step.stepId.toString(); + stepViewRow.stepName = step.stepDetails.stepName; + stepViewRow.stepId = step.stepDetails.id.toString(); return stepViewRow; }); this._showSteps = self._stepRows.length > 0; diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.ts b/src/sql/parts/jobManagement/views/jobStepsView.component.ts index 01988379f3..495e62abe8 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.ts @@ -66,7 +66,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, }, { verticalScrollMode: ScrollbarVisibility.Visible }); this._register(attachListStyler(this._tree, this.themeService)); } - this._tree.layout(JobStepsViewComponent._pageSize); + this._tree.layout(500); this._tree.setInput(new JobStepsViewModel()); $('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1'); $('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0'); @@ -74,8 +74,6 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, } ngOnInit() { - - let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; this._tree = new Tree(this._tableContainer.nativeElement, { controller: this._treeController, dataSource: this._treeDataSource, diff --git a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts index 9859f10611..49c021f4cd 100644 --- a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts +++ b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts @@ -14,7 +14,7 @@ import { AgentJobHistoryInfo } from 'sqlops'; import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities'; export class JobStepsViewRow { - public stepID: string; + public stepId: string; public stepName: string; public message: string; public rowID: string = generateUuid(); @@ -121,7 +121,7 @@ export class JobStepsViewRenderer implements tree.IRenderer { public renderElement(tree: tree.ITree, element: JobStepsViewRow, templateId: string, templateData: IListTemplate): void { let stepIdCol: HTMLElement = DOM.$('div'); stepIdCol.className = 'tree-id-col'; - stepIdCol.innerText = element.stepID; + stepIdCol.innerText = element.stepId; let stepNameCol: HTMLElement = DOM.$('div'); stepNameCol.className = 'tree-name-col'; stepNameCol.innerText = element.stepName; diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index 5a5738efb1..6625051cad 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -37,6 +37,7 @@ import { IDashboardService } from 'sql/services/dashboard/common/dashboardServic import { escape } from 'sql/base/common/strings'; import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { tableBackground, cellBackground, tableHoverBackground, jobsHeadingBackground, cellBorderColor } from 'sql/common/theme/colors'; +import { JobStepsViewRow } from 'sql/parts/jobManagement/views/jobStepsViewTree'; export const JOBSVIEW_SELECTOR: string = 'jobsview-component'; export const ROW_HEIGHT: number = 45; @@ -171,7 +172,6 @@ export class JobsViewComponent extends JobManagementView implements OnInit { }); this.rowDetail = rowDetail; columns.unshift(this.rowDetail.getColumnDefinition()); - let filterPlugin = new HeaderFilter({}, this._themeService); this.filterPlugin = filterPlugin; $(this._gridEl.nativeElement).empty(); @@ -475,7 +475,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit { case ('Failed'): resultIndicatorClass = 'jobview-jobnameindicatorfailure'; break; - case ('Canceled'): + case ('Cancelled'): resultIndicatorClass = 'jobview-jobnameindicatorcancel'; break; case ('Status Unknown'): @@ -523,17 +523,15 @@ export class JobsViewComponent extends JobManagementView implements OnInit { this.rowDetail.applyTemplateNewLineHeight(item, true); } - private loadJobHistories(): void { + private async loadJobHistories() { if (this.jobs) { - let erroredJobs = 0; let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; let separatedJobs = this.separateFailingJobs(); // grab histories of the failing jobs first // so they can be expanded quicker let failing = separatedJobs[0]; - this.curateJobHistory(failing, ownerUri); let passing = separatedJobs[1]; - this.curateJobHistory(passing, ownerUri); + Promise.all([this.curateJobHistory(failing, ownerUri), this.curateJobHistory(passing, ownerUri)]); } } @@ -578,11 +576,10 @@ export class JobsViewComponent extends JobManagementView implements OnInit { return job; } - private curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) { + private async curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) { const self = this; - for (let i = 0; i < jobs.length; i++) { - let job = jobs[i]; - this._jobManagementService.getJobHistory(ownerUri, job.jobId).then((result) => { + jobs.forEach(async (job) => { + await this._jobManagementService.getJobHistory(ownerUri, job.jobId).then((result) => { if (result && result.jobs) { self.jobHistories[job.jobId] = result.jobs; self._jobCacheObject.setJobHistory(job.jobId, result.jobs); @@ -600,7 +597,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit { } } }); - } + }); } private createJobChart(jobId: string, jobHistories: sqlops.AgentJobHistoryInfo[]): void { @@ -848,6 +845,40 @@ export class JobsViewComponent extends JobManagementView implements OnInit { return TPromise.as(actions); } + protected convertStepsToStepInfos(steps: sqlops.AgentJobStep[], job: sqlops.AgentJobInfo): sqlops.AgentJobStepInfo[] { + let result = []; + steps.forEach(step => { + let stepInfo: sqlops.AgentJobStepInfo = { + jobId: job.jobId, + jobName: job.name, + script: null, + scriptName: null, + stepName: step.stepName, + subSystem: null, + id: +step.stepId, + failureAction: null, + successAction: null, + failStepId: null, + successStepId: null, + command: null, + commandExecutionSuccessCode: null, + databaseName: null, + databaseUserName: null, + server: null, + outputFileName: null, + appendToLogFile: null, + appendToStepHist: null, + writeLogToTable: null, + appendLogToTable: null, + retryAttempts: null, + retryInterval: null, + proxyName: null + }; + result.push(stepInfo); + }); + return result; + } + protected getCurrentTableObject(rowIndex: number): any { let data = this._table.grid.getData(); if (!data || rowIndex >= data.getLength()) { @@ -855,10 +886,60 @@ export class JobsViewComponent extends JobManagementView implements OnInit { } let jobId = data.getItem(rowIndex).jobId; - let job = this.jobs.filter(job => { + if (!jobId) { + // if we couldn't find the ID, check if it's an + // error row + let isErrorRow: boolean = data.getItem(rowIndex).id.indexOf('error') >= 0; + if (isErrorRow) { + jobId = data.getItem(rowIndex - 1).jobId; + } + } + let job: sqlops.AgentJobInfo[] = this.jobs.filter(job => { return job.jobId === jobId; }); - + let jobHistories = this.jobHistories[jobId]; + let steps: sqlops.AgentJobStep[] = undefined; + let schedules: sqlops.AgentJobScheduleInfo[] = undefined; + let alerts: sqlops.AgentAlertInfo[] = undefined; + if (jobHistories && jobHistories[jobHistories.length-1]) { + // add steps + steps = jobHistories[jobHistories.length-1].steps; + if (steps && steps.length > 0) { + if (!job[0].JobSteps) { + job[0].JobSteps = []; + } + if (job[0].JobSteps.length !== steps.length) { + job[0].JobSteps = []; + steps.forEach(step => { + job[0].JobSteps.push(step.stepDetails); + }); + } + } + // add schedules + schedules = jobHistories[jobHistories.length-1].schedules; + if (schedules && schedules.length > 0) { + if (!job[0].JobSchedules) { + job[0].JobSchedules = []; + } + if (job[0].JobSchedules.length !== schedules.length) { + job[0].JobSchedules = []; + schedules.forEach(schedule => { + job[0].JobSchedules.push(schedule); + }); + } + } + // add alerts + alerts = jobHistories[jobHistories.length-1].alerts; + if (!job[0].Alerts) { + job[0].Alerts = []; + } + if (job[0].Alerts.length !== alerts.length) { + job[0].Alerts = []; + alerts.forEach(alert => { + job[0].Alerts.push(alert); + }); + } + } return job && job.length > 0 ? job[0] : undefined; } diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 2515752e79..401cc534e4 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -1313,6 +1313,7 @@ declare module 'sqlops' { jobCount: number; activeEndDate: string; scheduleUid: string; + description: string; } export interface AgentJobStep { @@ -1322,6 +1323,7 @@ declare module 'sqlops' { message: string; runDate: string; runStatus: number; + stepDetails: AgentJobStepInfo; } export interface AgentJobStepInfo { @@ -1369,6 +1371,8 @@ declare module 'sqlops' { retriesAttempted: string; server: string; steps: AgentJobStep[]; + schedules: AgentJobScheduleInfo[]; + alerts: AgentAlertInfo[]; } export interface AgentProxyInfo { From 6f39a37656726e6711e9510f80c9ffec14d054ed Mon Sep 17 00:00:00 2001 From: Aditya Bist Date: Thu, 4 Oct 2018 13:54:17 -0700 Subject: [PATCH 09/26] Agent/history update (#2756) * refresh history when job is run * refresh history and jobs when stopped --- .../parts/jobManagement/common/jobActions.ts | 23 ++++++++++++------- .../views/jobHistory.component.ts | 5 ++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/sql/parts/jobManagement/common/jobActions.ts b/src/sql/parts/jobManagement/common/jobActions.ts index ccb13a7bf8..fbd8b21317 100644 --- a/src/sql/parts/jobManagement/common/jobActions.ts +++ b/src/sql/parts/jobManagement/common/jobActions.ts @@ -16,6 +16,7 @@ import { JobsViewComponent } from '../views/jobsView.component'; import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.component'; import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component'; import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export enum JobActions { Run = 'run', @@ -38,7 +39,7 @@ export class JobsRefreshAction extends Action { super(JobsRefreshAction.ID, JobsRefreshAction.LABEL, 'refreshIcon'); } - public run(context: JobsViewComponent): TPromise { + public run(context: JobsViewComponent | JobHistoryComponent): TPromise { return new TPromise((resolve, reject) => { if (context) { context.refreshJobs(); @@ -77,7 +78,8 @@ export class RunJobAction extends Action { constructor( @INotificationService private notificationService: INotificationService, - @IJobManagementService private jobManagementService: IJobManagementService + @IJobManagementService private jobManagementService: IJobManagementService, + @IInstantiationService private instantationService: IInstantiationService ) { super(RunJobAction.ID, RunJobAction.LABEL, 'runJobIcon'); } @@ -85,9 +87,11 @@ export class RunJobAction extends Action { public run(context: JobHistoryComponent): TPromise { let jobName = context.agentJobInfo.name; let ownerUri = context.ownerUri; + let refreshAction = this.instantationService.createInstance(JobsRefreshAction); return new TPromise((resolve, reject) => { this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => { if (result.success) { + refreshAction.run(context); var startMsg = nls.localize('jobSuccessfullyStarted', ': The job was successfully started.'); this.notificationService.notify({ severity: Severity.Info, @@ -112,7 +116,8 @@ export class StopJobAction extends Action { constructor( @INotificationService private notificationService: INotificationService, - @IJobManagementService private jobManagementService: IJobManagementService + @IJobManagementService private jobManagementService: IJobManagementService, + @IInstantiationService private instantationService: IInstantiationService ) { super(StopJobAction.ID, StopJobAction.LABEL, 'stopJobIcon'); } @@ -120,14 +125,16 @@ export class StopJobAction extends Action { public run(context: JobHistoryComponent): TPromise { let jobName = context.agentJobInfo.name; let ownerUri = context.ownerUri; + let refreshAction = this.instantationService.createInstance(JobsRefreshAction); return new TPromise((resolve, reject) => { this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Stop).then(result => { if (result.success) { - var stopMsg = nls.localize('jobSuccessfullyStopped', ': The job was successfully stopped.'); - this.notificationService.notify({ - severity: Severity.Info, - message: jobName+ stopMsg - }); + refreshAction.run(context); + var stopMsg = nls.localize('jobSuccessfullyStopped', ': The job was successfully stopped.'); + this.notificationService.notify({ + severity: Severity.Info, + message: jobName+ stopMsg + }); resolve(true); } else { this.notificationService.notify({ diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index 7bb4953634..fc5971f574 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -315,6 +315,10 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { } } + public refreshJobs() { + this._agentViewComponent.refresh = true; + } + protected initActionBar() { let runJobAction = this.instantiationService.createInstance(RunJobAction); let stopJobAction = this.instantiationService.createInstance(StopJobAction); @@ -353,4 +357,5 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { this._showSteps = value; this._cd.detectChanges(); } + } From 004464c699f43b70a906152e58e60aa1a23d8dbb Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Thu, 4 Oct 2018 15:33:03 -0700 Subject: [PATCH 10/26] Fix tab color break caused by VS Code merge (#2761) --- src/sql/parts/editData/common/editDataInput.ts | 1 + src/sql/parts/query/common/queryInput.ts | 1 + src/vs/workbench/browser/parts/editor/tabsTitleControl.ts | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/sql/parts/editData/common/editDataInput.ts b/src/sql/parts/editData/common/editDataInput.ts index c53df2f445..833337ffec 100644 --- a/src/sql/parts/editData/common/editDataInput.ts +++ b/src/sql/parts/editData/common/editDataInput.ts @@ -186,6 +186,7 @@ export class EditDataInput extends EditorInput implements IConnectableInput { this._queryModelService.initializeEdit(this.uri, this.schemaName, this.tableName, this._objectType, rowLimit, queryString); this.showResultsEditor(); + this._onDidChangeLabel.fire(); } public onDisconnect(): void { diff --git a/src/sql/parts/query/common/queryInput.ts b/src/sql/parts/query/common/queryInput.ts index a095265580..bf430b32a0 100644 --- a/src/sql/parts/query/common/queryInput.ts +++ b/src/sql/parts/query/common/queryInput.ts @@ -246,6 +246,7 @@ export class QueryInput extends EditorInput implements IEncodingSupport, IConnec } } this._updateTaskbar.fire(); + this._onDidChangeLabel.fire(); } public onDisconnect(): void { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index d09c2ec571..85bde7ce9e 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -868,6 +868,9 @@ export class TabsTitleControl extends TitleControl { // Dirty State this.redrawEditorDirty(editor, tabContainer); + + // {{SQL CARBON EDIT}} -- Display the editor's tab color + this.setEditorTabColor(editor, tabContainer, this.group.isActive(editor)); } private redrawLabel(editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel, tabLabel: IEditorInputLabel): void { @@ -880,7 +883,8 @@ export class TabsTitleControl extends TitleControl { tabContainer.title = title; // Label - tabLabelWidget.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) }); + // {{SQL CARBON EDIT}} -- add title in options passed + tabLabelWidget.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), title }); // {{SQL CARBON EDIT}} -- Display the editor's tab color const isTabActive = this.group.isActive(editor); @@ -1105,10 +1109,12 @@ export class TabsTitleControl extends TitleControl { || this.themeService.getTheme().type === HIGH_CONTRAST || !sqlEditor.tabColor) { tabContainer.style.borderTopColor = ''; tabContainer.style.borderTopWidth = ''; + tabContainer.style.borderTopStyle = ''; return; } tabContainer.style.borderTopColor = sqlEditor.tabColor; tabContainer.style.borderTopWidth = isTabActive ? '3px' : '2px'; + tabContainer.style.borderTopStyle = 'solid'; if (tabColorMode === QueryConstants.tabColorModeFill) { let backgroundColor = Color.Format.CSS.parseHex(sqlEditor.tabColor); if (backgroundColor) { From e672fbf6e281777c7bd61ce57dfa63cf2351198d Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Fri, 5 Oct 2018 11:25:28 -0700 Subject: [PATCH 11/26] change selection on header selection to provide 1 selection not 1 million (#2757) --- .../browser/ui/table/plugins/additionalKeyBindings.plugin.ts | 2 -- .../base/browser/ui/table/plugins/rowNumberColumn.plugin.ts | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin.ts b/src/sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin.ts index 5711f8c049..35d7e318bb 100644 --- a/src/sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin.ts @@ -56,8 +56,6 @@ export class AdditionalKeyBindings implements Slick.Plugin { let selectionModel = this.grid.getSelectionModel(); if (selectionModel) { selectionModel.setSelectedRanges([new Slick.Range(0, 0, this.grid.getDataLength() - 1, this.grid.getColumns().length - 1)]); - } else { - this.grid.setSelectedRows(range(this.grid.getDataLength())); } } else { handled = false; diff --git a/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts index 7806e083f4..547dcd15f3 100644 --- a/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/rowNumberColumn.plugin.ts @@ -45,8 +45,9 @@ export class RowNumberColumn implements Slick.Plugin { private handleHeaderClick(e: MouseEvent, args: Slick.OnHeaderClickEventArgs): void { if (args.column.id === 'rowNumber') { this.grid.setActiveCell(0, 1); - if (this.grid.getSelectionModel()) { - this.grid.setSelectedRows(range(this.grid.getDataLength())); + let selectionModel = this.grid.getSelectionModel(); + if (selectionModel) { + selectionModel.setSelectedRanges([new Slick.Range(0, 0, this.grid.getDataLength() - 1, this.grid.getColumns().length - 1)]); } } } From 210447cd3716d0ad5c12e1a81649de3f9ac5ccf2 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Fri, 5 Oct 2018 12:40:56 -0700 Subject: [PATCH 12/26] Revert "Port the Azure Resource Explorer extension to core." (#2770) * Revert "change how query plan is handled (#2735)" This reverts commit 0693080630bbabebb9003aea5dfc41c6d3340e31. * Revert "center the icon (#2760)" This reverts commit 75d27837c26acba6897a6c794b7ed4569b901675. * Revert "Alanren/edit data improvement (#2748)" This reverts commit 597f29e90a1d47101e6cfab612dc4b76365627ff. * Revert "Port the Azure Resource Explorer extension to core. (#2701)" This reverts commit a77bb50b9edc6cfd4c7e83bb02ddb58dd8548af0. --- extensions/azurecore/.vscodeignore | 0 extensions/azurecore/README.md | 21 - extensions/azurecore/package.json | 128 ---- extensions/azurecore/package.nls.json | 13 - extensions/azurecore/resources/azure.svg | 6 - .../resources/dark/account_inverse.svg | 1 - .../resources/dark/connect_to_inverse.svg | 1 - .../resources/dark/filter_inverse.svg | 1 - .../resources/dark/folder_inverse.svg | 1 - .../resources/dark/refresh_inverse.svg | 1 - .../resources/dark/sql_database_inverse.svg | 1 - .../resources/dark/sql_server_inverse.svg | 1 - .../resources/dark/subscription_inverse.svg | 1 - .../azurecore/resources/light/account.svg | 1 - .../azurecore/resources/light/connect_to.svg | 1 - .../azurecore/resources/light/filter.svg | 1 - .../azurecore/resources/light/folder.svg | 1 - .../azurecore/resources/light/refresh.svg | 1 - .../resources/light/sql_database.svg | 1 - .../azurecore/resources/light/sql_server.svg | 1 - .../resources/light/subscription.svg | 1 - extensions/azurecore/src/apiWrapper.ts | 225 ------- extensions/azurecore/src/appContext.ts | 28 - .../azurecore/src/azureResource/commands.ts | 120 ---- .../azurecore/src/azureResource/constants.ts | 16 - .../azurecore/src/azureResource/errors.ts | 15 - .../azurecore/src/azureResource/interfaces.ts | 54 -- .../azurecore/src/azureResource/models.ts | 25 - .../src/azureResource/servicePool.ts | 35 - .../azureResource/services/accountService.ts | 32 - .../azureResource/services/cacheService.ts | 25 - .../azureResource/services/contextService.ts | 36 -- .../services/credentialService.ts | 43 -- .../services/databaseServerService.ts | 39 -- .../azureResource/services/databaseService.ts | 51 -- .../services/subscriptionFilterService.ts | 77 --- .../services/subscriptionService.ts | 33 - .../tree/accountNotSignedInTreeNode.ts | 51 -- .../src/azureResource/tree/accountTreeNode.ts | 155 ----- .../src/azureResource/tree/baseTreeNodes.ts | 71 -- .../tree/databaseContainerTreeNode.ts | 103 --- .../tree/databaseServerContainerTreeNode.ts | 103 --- .../tree/databaseServerTreeNode.ts | 57 -- .../azureResource/tree/databaseTreeNode.ts | 61 -- .../src/azureResource/tree/messageTreeNode.ts | 60 -- .../tree/subscriptionTreeNode.ts | 65 -- .../azureResource/tree/treeChangeHandler.ts | 12 - .../src/azureResource/tree/treeProvider.ts | 101 --- .../azurecore/src/azureResource/utils.ts | 44 -- extensions/azurecore/src/constants.ts | 8 - .../src/controllers/controllerBase.ts | 34 - .../src/controllers/mainController.ts | 54 -- extensions/azurecore/src/extension.ts | 40 -- .../tree/accountNotSignedInTreeNode.test.ts | 37 -- .../tree/accountTreeNode.test.ts | 294 --------- .../tree/databaseContainerTreeNode.test.ts | 219 ------- .../databaseServerContainerTreeNode.test.ts | 219 ------- .../tree/databaseServerTreeNode.test.ts | 62 -- .../tree/databaseTreeNode.test.ts | 62 -- .../tree/messageTreeNode.test.ts | 41 -- .../tree/subscriptionTreeNode.test.ts | 91 --- .../azureResource/tree/treeProvider.test.ts | 113 ---- extensions/azurecore/src/test/index.ts | 30 - extensions/azurecore/src/treeNodes.ts | 77 --- extensions/azurecore/src/typings/ref.d.ts | 9 - extensions/azurecore/tsconfig.json | 22 - extensions/azurecore/yarn.lock | 608 ------------------ scripts/test-integration.bat | 5 +- scripts/test-integration.sh | 4 +- .../common/connectionManagementService.ts | 10 +- 70 files changed, 5 insertions(+), 3955 deletions(-) delete mode 100644 extensions/azurecore/.vscodeignore delete mode 100644 extensions/azurecore/README.md delete mode 100644 extensions/azurecore/package.json delete mode 100644 extensions/azurecore/package.nls.json delete mode 100644 extensions/azurecore/resources/azure.svg delete mode 100644 extensions/azurecore/resources/dark/account_inverse.svg delete mode 100644 extensions/azurecore/resources/dark/connect_to_inverse.svg delete mode 100644 extensions/azurecore/resources/dark/filter_inverse.svg delete mode 100644 extensions/azurecore/resources/dark/folder_inverse.svg delete mode 100644 extensions/azurecore/resources/dark/refresh_inverse.svg delete mode 100644 extensions/azurecore/resources/dark/sql_database_inverse.svg delete mode 100644 extensions/azurecore/resources/dark/sql_server_inverse.svg delete mode 100644 extensions/azurecore/resources/dark/subscription_inverse.svg delete mode 100644 extensions/azurecore/resources/light/account.svg delete mode 100644 extensions/azurecore/resources/light/connect_to.svg delete mode 100644 extensions/azurecore/resources/light/filter.svg delete mode 100644 extensions/azurecore/resources/light/folder.svg delete mode 100644 extensions/azurecore/resources/light/refresh.svg delete mode 100644 extensions/azurecore/resources/light/sql_database.svg delete mode 100644 extensions/azurecore/resources/light/sql_server.svg delete mode 100644 extensions/azurecore/resources/light/subscription.svg delete mode 100644 extensions/azurecore/src/apiWrapper.ts delete mode 100644 extensions/azurecore/src/appContext.ts delete mode 100644 extensions/azurecore/src/azureResource/commands.ts delete mode 100644 extensions/azurecore/src/azureResource/constants.ts delete mode 100644 extensions/azurecore/src/azureResource/errors.ts delete mode 100644 extensions/azurecore/src/azureResource/interfaces.ts delete mode 100644 extensions/azurecore/src/azureResource/models.ts delete mode 100644 extensions/azurecore/src/azureResource/servicePool.ts delete mode 100644 extensions/azurecore/src/azureResource/services/accountService.ts delete mode 100644 extensions/azurecore/src/azureResource/services/cacheService.ts delete mode 100644 extensions/azurecore/src/azureResource/services/contextService.ts delete mode 100644 extensions/azurecore/src/azureResource/services/credentialService.ts delete mode 100644 extensions/azurecore/src/azureResource/services/databaseServerService.ts delete mode 100644 extensions/azurecore/src/azureResource/services/databaseService.ts delete mode 100644 extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts delete mode 100644 extensions/azurecore/src/azureResource/services/subscriptionService.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/accountTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/messageTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts delete mode 100644 extensions/azurecore/src/azureResource/tree/treeProvider.ts delete mode 100644 extensions/azurecore/src/azureResource/utils.ts delete mode 100644 extensions/azurecore/src/constants.ts delete mode 100644 extensions/azurecore/src/controllers/controllerBase.ts delete mode 100644 extensions/azurecore/src/controllers/mainController.ts delete mode 100644 extensions/azurecore/src/extension.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts delete mode 100644 extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts delete mode 100644 extensions/azurecore/src/test/index.ts delete mode 100644 extensions/azurecore/src/treeNodes.ts delete mode 100644 extensions/azurecore/src/typings/ref.d.ts delete mode 100644 extensions/azurecore/tsconfig.json delete mode 100644 extensions/azurecore/yarn.lock diff --git a/extensions/azurecore/.vscodeignore b/extensions/azurecore/.vscodeignore deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/extensions/azurecore/README.md b/extensions/azurecore/README.md deleted file mode 100644 index 003e776621..0000000000 --- a/extensions/azurecore/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Azure (Core) extension for Azure Data Studio - -Welcome to the Azure (Core) extension for Azure Data Studio! This extension supports core Azure functionality such as browsing and connecting to Azure data endpoints. In the current release the following features are supported: - -* Log in to Azure and browse your accounts, subscriptions and data resources -* See Azure SQL Databases and Servers in the tree, and open these connections in Object Explorer -* Filter the list of subscriptions for a given account, to make finding specific databases easier - -## Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## Privacy Statement - -The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. - -## License - -Copyright (c) Microsoft Corporation. All rights reserved. - -Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt). diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json deleted file mode 100644 index d490feffbb..0000000000 --- a/extensions/azurecore/package.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "name": "azurecore", - "displayName": "%azure.displayName%", - "description": "%azure.description", - "version": "0.1.0", - "publisher": "Microsoft", - "preview": true, - "engines": { - "vscode": "^1.25.0", - "sqlops": "*" - }, - "activationEvents": [ - "onView:azureResourceExplorer" - ], - "main": "./out/extension", - "contributes": { - "configuration": [ - { - "type": "object", - "title": "%azure.config.title%", - "properties": { - "azureResource.resourceFilter": { - "type": "array", - "default": null, - "description": "%azure.resourceFilter.description%" - } - } - } - ], - "commands": [ - { - "command": "azureresource.refreshall", - "title": "%azureresource.refreshall%", - "icon": { - "dark": "resources/dark/refresh_inverse.svg", - "light": "resources/light/refresh.svg" - } - }, - { - "command": "azureresource.refresh", - "title": "%azureresource.refresh%", - "icon": { - "dark": "resources/dark/refresh_inverse.svg", - "light": "resources/light/refresh.svg" - } - }, - { - "command": "azureresource.signin", - "title": "%azureresource.signin%" - }, - { - "command": "azureresource.connectsqldb", - "title": "%azureresource.connectsqldb%", - "icon": { - "dark": "resources/dark/connect_to_inverse.svg", - "light": "resources/light/connect_to.svg" - } - }, - { - "command": "azureresource.selectsubscriptions", - "title": "%azureresource.selectsubscriptions%", - "icon": { - "dark": "resources/dark/filter_inverse.svg", - "light": "resources/light/filter.svg" - } - } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "azureResource", - "title": "%azure.title%", - "icon": "resources/azure.svg" - } - ] - }, - "views": { - "azureResource": [ - { - "id": "azureResourceExplorer", - "name": "%azure.resourceExplorer.title%" - } - ] - }, - "menus": { - "view/title": [ - { - "command": "azureresource.refreshall", - "when": "view == azureResourceExplorer", - "group": "navigation@1" - } - ], - "view/item/context": [ - { - "command": "azureresource.connectsqldb", - "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", - "group": "1azureresource@1" - }, - { - "command": "azureresource.connectsqldb", - "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", - "group": "inline" - }, - { - "command": "azureresource.selectsubscriptions", - "when": "viewItem == azureResource.itemType.account", - "group": "inline" - }, - { - "command": "azureresource.refresh", - "when": "viewItem != azureResource.itemType.database && viewItem != azureResource.itemType.databaseServer && viewItem != azureResource.itemType.message", - "group": "inline" - } - ] - } - }, - "dependencies": { - "azure-arm-resource": "^7.0.0", - "azure-arm-sql": "^5.0.1", - "vscode-nls": "^4.0.0" - }, - "devDependencies": { - "@types/mocha": "^5.2.5", - "mocha": "^5.2.0", - "should": "^13.2.1", - "typemoq": "^2.1.0" - } -} diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json deleted file mode 100644 index 5b3abedb0b..0000000000 --- a/extensions/azurecore/package.nls.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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" -} \ No newline at end of file diff --git a/extensions/azurecore/resources/azure.svg b/extensions/azurecore/resources/azure.svg deleted file mode 100644 index dd17231563..0000000000 --- a/extensions/azurecore/resources/azure.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/extensions/azurecore/resources/dark/account_inverse.svg b/extensions/azurecore/resources/dark/account_inverse.svg deleted file mode 100644 index 0429cb4941..0000000000 --- a/extensions/azurecore/resources/dark/account_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -account_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/connect_to_inverse.svg b/extensions/azurecore/resources/dark/connect_to_inverse.svg deleted file mode 100644 index 9d796bdc76..0000000000 --- a/extensions/azurecore/resources/dark/connect_to_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -connect_to_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/filter_inverse.svg b/extensions/azurecore/resources/dark/filter_inverse.svg deleted file mode 100644 index 60b90abd6d..0000000000 --- a/extensions/azurecore/resources/dark/filter_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -filter_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/folder_inverse.svg b/extensions/azurecore/resources/dark/folder_inverse.svg deleted file mode 100644 index f94d427cb1..0000000000 --- a/extensions/azurecore/resources/dark/folder_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -folder_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/refresh_inverse.svg b/extensions/azurecore/resources/dark/refresh_inverse.svg deleted file mode 100644 index d79fdaa4e8..0000000000 --- a/extensions/azurecore/resources/dark/refresh_inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_database_inverse.svg b/extensions/azurecore/resources/dark/sql_database_inverse.svg deleted file mode 100644 index 5eaaf7e5f0..0000000000 --- a/extensions/azurecore/resources/dark/sql_database_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -sql_database_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_server_inverse.svg b/extensions/azurecore/resources/dark/sql_server_inverse.svg deleted file mode 100644 index 1ce3f48b39..0000000000 --- a/extensions/azurecore/resources/dark/sql_server_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -sql_server_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/subscription_inverse.svg b/extensions/azurecore/resources/dark/subscription_inverse.svg deleted file mode 100644 index 491fb5e895..0000000000 --- a/extensions/azurecore/resources/dark/subscription_inverse.svg +++ /dev/null @@ -1 +0,0 @@ -subscription_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/light/account.svg b/extensions/azurecore/resources/light/account.svg deleted file mode 100644 index 91de1865c0..0000000000 --- a/extensions/azurecore/resources/light/account.svg +++ /dev/null @@ -1 +0,0 @@ -account \ No newline at end of file diff --git a/extensions/azurecore/resources/light/connect_to.svg b/extensions/azurecore/resources/light/connect_to.svg deleted file mode 100644 index 1f420533d6..0000000000 --- a/extensions/azurecore/resources/light/connect_to.svg +++ /dev/null @@ -1 +0,0 @@ -connect_to \ No newline at end of file diff --git a/extensions/azurecore/resources/light/filter.svg b/extensions/azurecore/resources/light/filter.svg deleted file mode 100644 index 32f914f54a..0000000000 --- a/extensions/azurecore/resources/light/filter.svg +++ /dev/null @@ -1 +0,0 @@ -filter_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/folder.svg b/extensions/azurecore/resources/light/folder.svg deleted file mode 100644 index 517c9b185d..0000000000 --- a/extensions/azurecore/resources/light/folder.svg +++ /dev/null @@ -1 +0,0 @@ -folder_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/refresh.svg b/extensions/azurecore/resources/light/refresh.svg deleted file mode 100644 index e034574819..0000000000 --- a/extensions/azurecore/resources/light/refresh.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_database.svg b/extensions/azurecore/resources/light/sql_database.svg deleted file mode 100644 index e0c1584d98..0000000000 --- a/extensions/azurecore/resources/light/sql_database.svg +++ /dev/null @@ -1 +0,0 @@ -sql_database \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_server.svg b/extensions/azurecore/resources/light/sql_server.svg deleted file mode 100644 index 03cff4d932..0000000000 --- a/extensions/azurecore/resources/light/sql_server.svg +++ /dev/null @@ -1 +0,0 @@ -sql_server \ No newline at end of file diff --git a/extensions/azurecore/resources/light/subscription.svg b/extensions/azurecore/resources/light/subscription.svg deleted file mode 100644 index 5fda717712..0000000000 --- a/extensions/azurecore/resources/light/subscription.svg +++ /dev/null @@ -1 +0,0 @@ -subscription \ No newline at end of file diff --git a/extensions/azurecore/src/apiWrapper.ts b/extensions/azurecore/src/apiWrapper.ts deleted file mode 100644 index 7a13231e26..0000000000 --- a/extensions/azurecore/src/apiWrapper.ts +++ /dev/null @@ -1,225 +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 vscode from 'vscode'; -import * as sqlops from 'sqlops'; - -import * as constants from './constants'; - -/** - * Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into - * this API from our code - * - * @export - * @class ApiWrapper - */ -export class ApiWrapper { - // Data APIs - public registerConnectionProvider(provider: sqlops.ConnectionProvider): vscode.Disposable { - return sqlops.dataprotocol.registerConnectionProvider(provider); - } - - public registerObjectExplorerProvider(provider: sqlops.ObjectExplorerProvider): vscode.Disposable { - return sqlops.dataprotocol.registerObjectExplorerProvider(provider); - } - - public registerTaskServicesProvider(provider: sqlops.TaskServicesProvider): vscode.Disposable { - return sqlops.dataprotocol.registerTaskServicesProvider(provider); - } - - public registerFileBrowserProvider(provider: sqlops.FileBrowserProvider): vscode.Disposable { - return sqlops.dataprotocol.registerFileBrowserProvider(provider); - } - - public registerCapabilitiesServiceProvider(provider: sqlops.CapabilitiesProvider): vscode.Disposable { - return sqlops.dataprotocol.registerCapabilitiesServiceProvider(provider); - } - - public registerModelViewProvider(widgetId: string, handler: (modelView: sqlops.ModelView) => void): void { - return sqlops.ui.registerModelViewProvider(widgetId, handler); - } - - public registerWebviewProvider(widgetId: string, handler: (webview: sqlops.DashboardWebview) => void): void { - return sqlops.dashboard.registerWebviewProvider(widgetId, handler); - } - - public createDialog(title: string): sqlops.window.modelviewdialog.Dialog { - return sqlops.window.modelviewdialog.createDialog(title); - } - - public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { - return sqlops.window.modelviewdialog.openDialog(dialog); - } - - public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { - return sqlops.window.modelviewdialog.closeDialog(dialog); - } - - public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void { - sqlops.tasks.registerTask(taskId, handler); - } - - public startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void { - sqlops.tasks.startBackgroundOperation(operationInfo); - } - - public getActiveConnections(): Thenable { - return sqlops.connection.getActiveConnections(); - } - - public getCurrentConnection(): Thenable { - return sqlops.connection.getCurrentConnection(); - } - - public createModelViewEditor(title: string, options?: sqlops.ModelViewEditorOptions): sqlops.workspace.ModelViewEditor { - return sqlops.workspace.createModelViewEditor(title, options); - } - - // VSCode APIs - public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { - return vscode.window.createTerminal(name, shellPath, shellArgs); - } - - public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { - return vscode.window.createTerminal(options); - } - - public executeCommand(command: string, ...rest: any[]): Thenable { - return vscode.commands.executeCommand(command, ...rest); - } - - public getFilePathRelativeToWorkspace(uri: vscode.Uri): string { - return vscode.workspace.asRelativePath(uri); - } - - public getWorkspaceFolders(): vscode.WorkspaceFolder[] { - return vscode.workspace.workspaceFolders; - } - - public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined { - let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); - return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; - } - - public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { - return vscode.commands.registerCommand(command, callback, thisArg); - } - - public registerDocumentOpenHandler(handler: (doc: vscode.TextDocument) => any): vscode.Disposable { - return vscode.workspace.onDidOpenTextDocument(handler); - } - - public registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { - return vscode.window.registerTreeDataProvider(viewId, treeDataProvider); - } - - public setCommandContext(key: string, value: any): Thenable { - return vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, key, value); - } - - /** - * Get the configuration for a extensionName - * @param extensionName The string name of the extension to get the configuration for - * @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations - */ - public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration { - if (typeof resource === 'string') { - try { - resource = this.parseUri(resource); - } catch (e) { - resource = undefined; - } - } - return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri); - } - - public getExtensionConfiguration(): vscode.WorkspaceConfiguration { - return this.getConfiguration(constants.extensionConfigSectionName); - } - - /** - * Parse uri - */ - public parseUri(uri: string): vscode.Uri { - return vscode.Uri.parse(uri); - } - - public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { - return vscode.window.showOpenDialog(options); - } - - public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { - return vscode.window.showSaveDialog(options); - } - - public openTextDocument(uri: vscode.Uri): Thenable; - public openTextDocument(options: { language?: string; content?: string; }): Thenable; - public openTextDocument(uriOrOptions): Thenable { - return vscode.workspace.openTextDocument(uriOrOptions); - } - - public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean, preview?: boolean): Thenable { - let options: vscode.TextDocumentShowOptions = { - viewColumn: column, - preserveFocus: preserveFocus, - preview: preview - }; - return vscode.window.showTextDocument(document, options); - } - - public showErrorMessage(message: string, ...items: string[]): Thenable { - return vscode.window.showErrorMessage(message, ...items); - } - - public showWarningMessage(message: string, ...items: string[]): Thenable { - return vscode.window.showWarningMessage(message, ...items); - } - - public showInformationMessage(message: string, ...items: string[]): Thenable { - return vscode.window.showInformationMessage(message, ...items); - } - - public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { - return vscode.window.createStatusBarItem(alignment, priority); - } - - public get workspaceFolders(): vscode.WorkspaceFolder[] { - return vscode.workspace.workspaceFolders; - } - - public createOutputChannel(name: string): vscode.OutputChannel { - return vscode.window.createOutputChannel(name); - } - - public createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage { - return sqlops.window.modelviewdialog.createWizardPage(title); - } - - public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { - return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters); - } - - public createTab(title: string): sqlops.window.modelviewdialog.DialogTab { - return sqlops.window.modelviewdialog.createTab(title); - } - - // Account APIs - public getAllAccounts(): Thenable { - return sqlops.accounts.getAllAccounts(); - } - - public getSecurityToken(account: sqlops.Account): Thenable<{}> { - return sqlops.accounts.getSecurityToken(account); - } - - public readonly onDidChangeAccounts = sqlops.accounts.onDidChangeAccounts; - - // Connection APIs - public openConnectionDialog(providers: string[], initialConnectionProfile?: sqlops.IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Thenable { - return sqlops.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions); - } -} diff --git a/extensions/azurecore/src/appContext.ts b/extensions/azurecore/src/appContext.ts deleted file mode 100644 index 4abec27439..0000000000 --- a/extensions/azurecore/src/appContext.ts +++ /dev/null @@ -1,28 +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 vscode from 'vscode'; -import { ApiWrapper } from './apiWrapper'; - -/** - * Global context for the application - */ -export class AppContext { - - private serviceMap: Map = new Map(); - constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) { - this.apiWrapper = apiWrapper || new ApiWrapper(); - } - - public getService(serviceName: string): T { - return this.serviceMap.get(serviceName) as T; - } - - public registerService(serviceName: string, service: T): void { - this.serviceMap.set(serviceName, service); - } -} diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts deleted file mode 100644 index 0edb925c6f..0000000000 --- a/extensions/azurecore/src/azureResource/commands.ts +++ /dev/null @@ -1,120 +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 { window, QuickPickItem } from 'vscode'; -import { IConnectionProfile } from 'sqlops'; -import { generateGuid } from './utils'; -import { ApiWrapper } from '../apiWrapper'; -import { TreeNode } from '../treeNodes'; - -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'; - -export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: AzureResourceTreeProvider): void { - apiWrapper.registerCommand('azureresource.selectsubscriptions', async (node?: TreeNode) => { - if (!(node instanceof AzureResourceAccountTreeNode)) { - return; - } - - const accountNode = node as AzureResourceAccountTreeNode; - - const servicePool = AzureResourceServicePool.getInstance(); - - let subscriptions = await accountNode.getCachedSubscriptions(); - if (!subscriptions || subscriptions.length === 0) { - const credentials = await servicePool.credentialService.getCredentials(accountNode.account); - subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials); - } - - const selectedSubscriptions = (await servicePool.subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || []; - const selectedSubscriptionIds: string[] = []; - if (selectedSubscriptions.length > 0) { - selectedSubscriptionIds.push(...selectedSubscriptions.map((subscription) => subscription.id)); - } else { - // ALL subscriptions are selected by default - selectedSubscriptionIds.push(...subscriptions.map((subscription) => subscription.id)); - } - - interface SubscriptionQuickPickItem extends QuickPickItem { - subscription: AzureResourceSubscription; - } - - const subscriptionItems: SubscriptionQuickPickItem[] = subscriptions.map((subscription) => { - return { - label: subscription.name, - picked: selectedSubscriptionIds.indexOf(subscription.id) !== -1, - subscription: subscription - }; - }); - - const pickedSubscriptionItems = (await window.showQuickPick(subscriptionItems, { canPickMany: true })); - if (pickedSubscriptionItems && pickedSubscriptionItems.length > 0) { - tree.refresh(node, false); - - const pickedSubscriptions = pickedSubscriptionItems.map((subscriptionItem) => subscriptionItem.subscription); - await servicePool.subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, pickedSubscriptions); - } - }); - - apiWrapper.registerCommand('azureresource.refreshall', () => tree.notifyNodeChanged(undefined)); - - apiWrapper.registerCommand('azureresource.refresh', async (node?: TreeNode) => { - tree.refresh(node, true); - }); - - apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => { - let connectionProfile: 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'); - }); -} diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts deleted file mode 100644 index 3fb5368927..0000000000 --- a/extensions/azurecore/src/azureResource/constants.ts +++ /dev/null @@ -1,16 +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'; - -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' -} diff --git a/extensions/azurecore/src/azureResource/errors.ts b/extensions/azurecore/src/azureResource/errors.ts deleted file mode 100644 index c0fe4a842f..0000000000 --- a/extensions/azurecore/src/azureResource/errors.ts +++ /dev/null @@ -1,15 +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'; - -export class AzureResourceCredentialError extends Error { - constructor( - message: string, - public innerError: Error - ) { - super(message); - } -} diff --git a/extensions/azurecore/src/azureResource/interfaces.ts b/extensions/azurecore/src/azureResource/interfaces.ts deleted file mode 100644 index 5d21c5d408..0000000000 --- a/extensions/azurecore/src/azureResource/interfaces.ts +++ /dev/null @@ -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 { ServiceClientCredentials } from 'ms-rest'; -import { Account, DidChangeAccountsParams } from 'sqlops'; -import { Event } from 'vscode'; - -import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models'; - -export interface IAzureResourceAccountService { - getAccounts(): Promise; - - readonly onDidChangeAccounts: Event; -} - -export interface IAzureResourceCredentialService { - getCredentials(account: Account): Promise; -} - -export interface IAzureResourceSubscriptionService { - getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise; -} - -export interface IAzureResourceSubscriptionFilterService { - getSelectedSubscriptions(account: Account): Promise; - - saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise; -} - -export interface IAzureResourceDatabaseServerService { - getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; -} - -export interface IAzureResourceDatabaseService { - getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; -} - -export interface IAzureResourceCacheService { - get(key: string): T | undefined; - - update(key: string, value: T): void; -} - -export interface IAzureResourceContextService { - getAbsolutePath(relativePath: string): string; - - executeCommand(commandId: string, ...args: any[]): void; - - showErrorMessage(errorMessage: string): void; -} diff --git a/extensions/azurecore/src/azureResource/models.ts b/extensions/azurecore/src/azureResource/models.ts deleted file mode 100644 index 21e5b4ab37..0000000000 --- a/extensions/azurecore/src/azureResource/models.ts +++ /dev/null @@ -1,25 +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'; - -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; -} diff --git a/extensions/azurecore/src/azureResource/servicePool.ts b/extensions/azurecore/src/azureResource/servicePool.ts deleted file mode 100644 index 27aff58d98..0000000000 --- a/extensions/azurecore/src/azureResource/servicePool.ts +++ /dev/null @@ -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(); -} diff --git a/extensions/azurecore/src/azureResource/services/accountService.ts b/extensions/azurecore/src/azureResource/services/accountService.ts deleted file mode 100644 index bc38f823a5..0000000000 --- a/extensions/azurecore/src/azureResource/services/accountService.ts +++ /dev/null @@ -1,32 +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 { Event } from 'vscode'; -import { Account, DidChangeAccountsParams } from 'sqlops'; -import { ApiWrapper } from '../../apiWrapper'; - -import { IAzureResourceAccountService } from '../interfaces'; - -export class AzureResourceAccountService implements IAzureResourceAccountService { - public constructor( - apiWrapper: ApiWrapper - ) { - this._apiWrapper = apiWrapper; - this._onDidChangeAccounts = this._apiWrapper.onDidChangeAccounts; - } - - public async getAccounts(): Promise { - return await this._apiWrapper.getAllAccounts(); - } - - public get onDidChangeAccounts(): Event { - return this._onDidChangeAccounts; - } - - private _apiWrapper: ApiWrapper = undefined; - private _onDidChangeAccounts: Event = undefined; -} diff --git a/extensions/azurecore/src/azureResource/services/cacheService.ts b/extensions/azurecore/src/azureResource/services/cacheService.ts deleted file mode 100644 index ba0d100273..0000000000 --- a/extensions/azurecore/src/azureResource/services/cacheService.ts +++ /dev/null @@ -1,25 +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 { IAzureResourceCacheService } from "../interfaces"; - -export class AzureResourceCacheService implements IAzureResourceCacheService { - public constructor( - public readonly context: ExtensionContext - ) { - } - - public get(key: string): T | undefined { - return this.context.workspaceState.get(key); - } - - public update(key: string, value: T): void { - this.context.workspaceState.update(key, value); - } -} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/services/contextService.ts b/extensions/azurecore/src/azureResource/services/contextService.ts deleted file mode 100644 index 0e153d48e5..0000000000 --- a/extensions/azurecore/src/azureResource/services/contextService.ts +++ /dev/null @@ -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; -} diff --git a/extensions/azurecore/src/azureResource/services/credentialService.ts b/extensions/azurecore/src/azureResource/services/credentialService.ts deleted file mode 100644 index 028181fb87..0000000000 --- a/extensions/azurecore/src/azureResource/services/credentialService.ts +++ /dev/null @@ -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 { Account } 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: Account): Promise { - try { - let credentials: TokenCredentials[] = []; - let tokens = await this._apiWrapper.getSecurityToken(account); - - 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; -} diff --git a/extensions/azurecore/src/azureResource/services/databaseServerService.ts b/extensions/azurecore/src/azureResource/services/databaseServerService.ts deleted file mode 100644 index 9145629a34..0000000000 --- a/extensions/azurecore/src/azureResource/services/databaseServerService.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/extensions/azurecore/src/azureResource/services/databaseService.ts b/extensions/azurecore/src/azureResource/services/databaseService.ts deleted file mode 100644 index 643db5c1e6..0000000000 --- a/extensions/azurecore/src/azureResource/services/databaseService.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts deleted file mode 100644 index 9e6bea1b08..0000000000 --- a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts +++ /dev/null @@ -1,77 +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 { WorkspaceConfiguration, ConfigurationTarget } from 'vscode'; -import { Account } from 'sqlops'; - -import { IAzureResourceSubscriptionFilterService, IAzureResourceCacheService } from '../interfaces'; -import { AzureResourceSubscription } from '../models'; - -interface AzureResourceSelectedSubscriptionsCache { - selectedSubscriptions: { [accountId: string]: AzureResourceSubscription[]}; -} - -export class AzureResourceSubscriptionFilterService implements IAzureResourceSubscriptionFilterService { - public constructor( - cacheService: IAzureResourceCacheService - ) { - this._cacheService = cacheService; - } - - public async getSelectedSubscriptions(account: Account): Promise { - let selectedSubscriptions: AzureResourceSubscription[] = []; - - const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); - if (cache) { - selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId]; - } - - return selectedSubscriptions; - } - - public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise { - let selectedSubscriptionsCache: { [accountId: string]: AzureResourceSubscription[]} = {}; - - const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); - if (cache) { - selectedSubscriptionsCache = cache.selectedSubscriptions; - } - - if (!selectedSubscriptionsCache) { - selectedSubscriptionsCache = {}; - } - - selectedSubscriptionsCache[account.key.accountId] = selectedSubscriptions; - - this._cacheService.update(AzureResourceSubscriptionFilterService.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(AzureResourceSubscriptionFilterService.FilterConfigName); - let configTarget = ConfigurationTarget.Global; - if (resourceFilterConfig) { - if (resourceFilterConfig.workspaceFolderValue) { - configTarget = ConfigurationTarget.WorkspaceFolder; - } else if (resourceFilterConfig.workspaceValue) { - configTarget = ConfigurationTarget.Workspace; - } else if (resourceFilterConfig.globalValue) { - configTarget = ConfigurationTarget.Global; - } - } - - await this._config.update(AzureResourceSubscriptionFilterService.FilterConfigName, filters, configTarget); - } - - private _config: WorkspaceConfiguration = undefined; - private _cacheService: IAzureResourceCacheService = undefined; - - private static readonly FilterConfigName = 'resourceFilter'; - private static readonly CacheKey = 'azureResource.cache.selectedSubscriptions'; -} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionService.ts b/extensions/azurecore/src/azureResource/services/subscriptionService.ts deleted file mode 100644 index 9a8cfe0779..0000000000 --- a/extensions/azurecore/src/azureResource/services/subscriptionService.ts +++ /dev/null @@ -1,33 +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 { Account } from 'sqlops'; -import { ServiceClientCredentials } from 'ms-rest'; -import { SubscriptionClient } from 'azure-arm-resource'; - -import { IAzureResourceSubscriptionService } from '../interfaces'; -import { AzureResourceSubscription } from '../models'; - -export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService { - public async getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise { - 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. - } - } - - return subscriptions; - } -} diff --git a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts deleted file mode 100644 index 47008428aa..0000000000 --- a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts +++ /dev/null @@ -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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { NodeInfo } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -import { AzureResourceItemType } from '../constants'; - -export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { - public getChildren(): TreeNode[] | Promise { - return []; - } - - public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.SignInLabel, TreeItemCollapsibleState.None); - item.contextValue = AzureResourceItemType.message; - item.command = { - title: AzureResourceAccountNotSignedInTreeNode.SignInLabel, - command: 'azureresource.signin', - arguments: [this] - }; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: AzureResourceAccountNotSignedInTreeNode.SignInLabel, - isLeaf: true, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.message, - nodeSubType: undefined, - iconType: AzureResourceItemType.message - }; - } - - public get nodePathValue(): string { - return 'message_accountNotSignedIn'; - } - - private static readonly SignInLabel = localize('azureResource.tree.accountNotSignedInTreeNode.signIn', 'Sign in to Azure ...'); -} diff --git a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts deleted file mode 100644 index a95a485ece..0000000000 --- a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts +++ /dev/null @@ -1,155 +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 { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode'; -import { AzureResourceMessageTreeNode } from './messageTreeNode'; -import { AzureResourceErrorMessageUtil } from '../utils'; -import { AzureResourceSubscription } from '../models'; -import { IAzureResourceTreeChangeHandler } from './treeProvider'; - -export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase { - public constructor( - account: Account, - treeChangeHandler: IAzureResourceTreeChangeHandler - ) { - super(account, treeChangeHandler, undefined); - - this._id = `account_${this.account.key.accountId}`; - this._label = this.generateLabel(); - } - - public async getChildren(): Promise { - try { - let subscriptions: AzureResourceSubscription[] = []; - - if (this._isClearingCache) { - const credentials = await this.getCredentials(); - subscriptions = (await this.servicePool.subscriptionService.getSubscriptions(this.account, credentials)) || []; - - let cache = this.getCache(); - if (!cache) { - cache = { subscriptions: { } }; - } - cache.subscriptions[this.account.key.accountId] = subscriptions; - this.updateCache(cache); - - this._isClearingCache = false; - } else { - subscriptions = await this.getCachedSubscriptions(); - } - - this._totalSubscriptionCount = subscriptions.length; - - let selectedSubscriptions = await this.servicePool.subscriptionFilterService.getSelectedSubscriptions(this.account); - let selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id); - if (selectedSubscriptionIds.length > 0) { - subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1); - this._selectedSubscriptionCount = selectedSubscriptionIds.length; - } else { - // ALL subscriptions are listed by default - this._selectedSubscriptionCount = this._totalSubscriptionCount; - } - - this.refreshLabel(); - - if (subscriptions.length === 0) { - return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.NoSubscriptions, this)]; - } else { - return subscriptions.map((subscription) => new AzureResourceSubscriptionTreeNode(subscription, this.account, this.treeChangeHandler, this)); - } - } catch (error) { - return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; - } - } - - public async getCachedSubscriptions(): Promise { - const subscriptions: AzureResourceSubscription[] = []; - const cache = this.getCache(); - if (cache) { - subscriptions.push(...cache.subscriptions[this.account.key.accountId]); - } - return subscriptions; - } - - public getTreeItem(): TreeItem | Promise { - let 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') - }; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: this._label, - isLeaf: false, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.account, - nodeSubType: undefined, - iconType: AzureResourceItemType.account - }; - } - - public get nodePathValue(): string { - return this._id; - } - - public get totalSubscriptionCount(): number { - return this._totalSubscriptionCount; - } - - public get selectedSubscriptionCount(): number { - return this._selectedSubscriptionCount; - } - - protected refreshLabel(): void { - const newLabel = this.generateLabel(); - if (this._label !== newLabel) { - this._label = newLabel; - this.treeChangeHandler.notifyNodeChanged(this); - } - } - - protected get cacheKey(): string { - return 'azureResource.cache.subscriptions'; - } - - private generateLabel(): string { - let label = `${this.account.displayInfo.displayName} (${this.account.key.accountId})`; - - if (this._totalSubscriptionCount !== 0) { - label += ` (${this._selectedSubscriptionCount} / ${this._totalSubscriptionCount} subscriptions)`; - } - - return label; - } - - 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[] }; -} diff --git a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts deleted file mode 100644 index ecea876ec1..0000000000 --- a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts +++ /dev/null @@ -1,71 +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 { Account } from 'sqlops'; -import { ServiceClientCredentials } from 'ms-rest'; -import { TreeNode } from '../../treeNodes'; - -import { AzureResourceServicePool } from '../servicePool'; -import { AzureResourceCredentialError } from '../errors'; -import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; - -export abstract class AzureResourceTreeNodeBase extends TreeNode { - public constructor( - public readonly treeChangeHandler: IAzureResourceTreeChangeHandler, - parent: TreeNode - ) { - super(); - - this.parent = parent; - } - - public readonly servicePool = AzureResourceServicePool.getInstance(); -} - -export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase { - public constructor( - public readonly account: Account, - treeChangeHandler: IAzureResourceTreeChangeHandler, - parent: TreeNode - ) { - super(treeChangeHandler, parent); - } - - public clearCache(): void { - this._isClearingCache = true; - } - - public get isClearingCache(): boolean { - return this._isClearingCache; - } - - protected async getCredentials(): Promise { - try { - return await this.servicePool.credentialService.getCredentials(this.account); - } catch (error) { - if (error instanceof AzureResourceCredentialError) { - this.servicePool.contextService.showErrorMessage(error.message); - - this.servicePool.contextService.executeCommand('azureresource.signin'); - } else { - throw error; - } - } - } - - protected updateCache(cache: T): void { - this.servicePool.cacheService.update(this.cacheKey, cache); - } - - protected getCache(): T { - return this.servicePool.cacheService.get(this.cacheKey); - } - - protected abstract get cacheKey(): string; - - protected _isClearingCache = true; -} diff --git a/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts deleted file mode 100644 index 26d5f6d70c..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts +++ /dev/null @@ -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 { - try { - let databases: AzureResourceDatabase[] = []; - - if (this._isClearingCache) { - let credentials = await this.getCredentials(); - databases = (await this.servicePool.databaseService.getDatabases(this.subscription, credentials)) || []; - - let cache = this.getCache(); - if (!cache) { - cache = { databases: { } }; - } - cache.databases[this.subscription.id] = databases; - this.updateCache(cache); - - this._isClearingCache = false; - } else { - const cache = this.getCache(); - if (cache) { - databases = cache.databases[this.subscription.id] || []; - } - } - - 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 { - 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[] }; -} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts deleted file mode 100644 index 6f2921e092..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts +++ /dev/null @@ -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 { - try { - let databaseServers: AzureResourceDatabaseServer[] = []; - - if (this._isClearingCache) { - let credentials = await this.getCredentials(); - databaseServers = (await this.servicePool.databaseServerService.getDatabaseServers(this.subscription, credentials)) || []; - - let cache = this.getCache(); - if (!cache) { - cache = { databaseServers: { } }; - } - cache.databaseServers[this.subscription.id] = databaseServers; - this.updateCache(cache); - - this._isClearingCache = false; - } else { - const cache = this.getCache(); - if (cache) { - databaseServers = cache.databaseServers[this.subscription.id] || []; - } - } - - 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 { - 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[] }; -} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts deleted file mode 100644 index 2c8cf5bd4b..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts +++ /dev/null @@ -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 { - return []; - } - - public getTreeItem(): TreeItem | Promise { - 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}`; - } -} diff --git a/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts deleted file mode 100644 index 1089a6e0c5..0000000000 --- a/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts +++ /dev/null @@ -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 { - return []; - } - - public getTreeItem(): TreeItem | Promise { - 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; -} diff --git a/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts b/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts deleted file mode 100644 index edea2e5a76..0000000000 --- a/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts +++ /dev/null @@ -1,60 +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 { AzureResourceItemType } from '../constants'; - -export class AzureResourceMessageTreeNode extends TreeNode { - public constructor( - public readonly message: string, - parent: TreeNode - ) { - super(); - - this.parent = parent; - this._id = `message_${AzureResourceMessageTreeNode._messageNum++}`; - } - - public static create(message: string, parent: TreeNode): AzureResourceMessageTreeNode { - return new AzureResourceMessageTreeNode(message, parent); - } - - public getChildren(): TreeNode[] | Promise { - return []; - } - - public getTreeItem(): TreeItem | Promise { - let item = new TreeItem(this.message, TreeItemCollapsibleState.None); - item.contextValue = AzureResourceItemType.message; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: this.message, - isLeaf: true, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.message, - nodeSubType: undefined, - iconType: AzureResourceItemType.message - }; - } - - public get nodePathValue(): string { - return this._id; - } - - private _id: string; - - private static _messageNum: number = 0; -} diff --git a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts deleted file mode 100644 index ff8474ca3f..0000000000 --- a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts +++ /dev/null @@ -1,65 +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 { AzureResourceTreeNodeBase, AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; -import { AzureResourceItemType } from '../constants'; -import { AzureResourceDatabaseContainerTreeNode } from './databaseContainerTreeNode'; -import { AzureResourceDatabaseServerContainerTreeNode } from './databaseServerContainerTreeNode'; -import { AzureResourceSubscription } from '../models'; -import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; - -export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase { - public constructor( - public readonly subscription: AzureResourceSubscription, - account: Account, - treeChangeHandler: IAzureResourceTreeChangeHandler, - parent: TreeNode - ) { - super(treeChangeHandler, parent); - - this._children.push(new AzureResourceDatabaseContainerTreeNode(subscription, account, treeChangeHandler, this)); - this._children.push(new AzureResourceDatabaseServerContainerTreeNode(subscription, account, treeChangeHandler, this)); - } - - public async getChildren(): Promise { - return this._children; - } - - public getTreeItem(): TreeItem | Promise { - let 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') - }; - return item; - } - - public getNodeInfo(): NodeInfo { - return { - label: this.subscription.name, - isLeaf: false, - errorMessage: undefined, - metadata: undefined, - nodePath: this.generateNodePath(), - nodeStatus: undefined, - nodeType: AzureResourceItemType.subscription, - nodeSubType: undefined, - iconType: AzureResourceItemType.subscription - }; - } - - public get nodePathValue(): string { - return `subscription_${this.subscription.id}`; - } - - private _children: AzureResourceContainerTreeNodeBase[] = []; -} diff --git a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts deleted file mode 100644 index b7db2ed6b4..0000000000 --- a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts +++ /dev/null @@ -1,12 +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 { TreeNode } from '../../treeNodes'; - -export interface IAzureResourceTreeChangeHandler { - notifyNodeChanged(node: TreeNode): void; -} diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts deleted file mode 100644 index 7cfd4ce89f..0000000000 --- a/extensions/azurecore/src/azureResource/tree/treeProvider.ts +++ /dev/null @@ -1,101 +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 { TreeDataProvider, EventEmitter, Event, TreeItem } from 'vscode'; -import { DidChangeAccountsParams } from 'sqlops'; -import { TreeNode } from '../../treeNodes'; -import { setInterval, clearInterval } from 'timers'; -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -import { AzureResourceServicePool } from '../servicePool'; -import { AzureResourceAccountTreeNode } from './accountTreeNode'; -import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode'; -import { AzureResourceMessageTreeNode } from './messageTreeNode'; -import { AzureResourceContainerTreeNodeBase, AzureResourceTreeNodeBase } from './baseTreeNodes'; -import { AzureResourceErrorMessageUtil } from '../utils'; - -export interface IAzureResourceTreeChangeHandler { - notifyNodeChanged(node: TreeNode): void; -} - -export class AzureResourceTreeProvider implements TreeDataProvider, IAzureResourceTreeChangeHandler { - public constructor() { - AzureResourceServicePool.getInstance().accountService.onDidChangeAccounts((e: DidChangeAccountsParams) => { this._onDidChangeTreeData.fire(undefined); }); - } - - public async getChildren(element?: TreeNode): Promise { - if (element) { - return element.getChildren(true); - } - - if (!this.isSystemInitialized) { - this._loadingTimer = setInterval(async () => { - try { - // Call sqlops.accounts.getAllAccounts() to determine whether the system has been initialized. - await AzureResourceServicePool.getInstance().accountService.getAccounts(); - - // System has been initialized - this.isSystemInitialized = true; - - if (this._loadingTimer) { - clearInterval(this._loadingTimer); - } - - this._onDidChangeTreeData.fire(undefined); - } catch (error) { - // System not initialized yet - this.isSystemInitialized = false; - } - }, AzureResourceTreeProvider.LoadingTimerInterval); - - return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.Loading, undefined)]; - } - - try { - const accounts = await AzureResourceServicePool.getInstance().accountService.getAccounts(); - - if (accounts && accounts.length > 0) { - return accounts.map((account) => new AzureResourceAccountTreeNode(account, this)); - } else { - return [new AzureResourceAccountNotSignedInTreeNode()]; - } - } catch (error) { - return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), undefined)]; - } - } - - public get onDidChangeTreeData(): Event { - return this._onDidChangeTreeData.event; - } - - public notifyNodeChanged(node: TreeNode): void { - this._onDidChangeTreeData.fire(node); - } - - public async refresh(node: TreeNode, isClearingCache: boolean): Promise { - if (isClearingCache) { - if ((node instanceof AzureResourceContainerTreeNodeBase)) { - node.clearCache(); - } - } - - this._onDidChangeTreeData.fire(node); - } - - public getTreeItem(element: TreeNode): TreeItem | Thenable { - return element.getTreeItem(); - } - - public isSystemInitialized: boolean = false; - - private _loadingTimer: NodeJS.Timer = undefined; - private _onDidChangeTreeData = new EventEmitter(); - - private static readonly Loading = localize('azureResource.tree.treeProvider.loading', 'Loading ...'); - private static readonly LoadingTimerInterval = 5000; -} diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts deleted file mode 100644 index 99f177eafb..0000000000 --- a/extensions/azurecore/src/azureResource/utils.ts +++ /dev/null @@ -1,44 +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 nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -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)); - } -} - -export function generateGuid(): string { - let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; - // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) - let oct: string = ''; - let tmp: number; - /* tslint:disable:no-bitwise */ - for (let a: number = 0; a < 4; a++) { - tmp = (4294967296 * Math.random()) | 0; - oct += hexValues[tmp & 0xF] + - hexValues[tmp >> 4 & 0xF] + - hexValues[tmp >> 8 & 0xF] + - hexValues[tmp >> 12 & 0xF] + - hexValues[tmp >> 16 & 0xF] + - hexValues[tmp >> 20 & 0xF] + - hexValues[tmp >> 24 & 0xF] + - hexValues[tmp >> 28 & 0xF]; - } - - // 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively' - let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0]; - return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12); - /* tslint:enable:no-bitwise */ -} \ No newline at end of file diff --git a/extensions/azurecore/src/constants.ts b/extensions/azurecore/src/constants.ts deleted file mode 100644 index 994cfc1f6a..0000000000 --- a/extensions/azurecore/src/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -export const extensionConfigSectionName = 'azure'; -export const ViewType = 'view'; - -export enum BuiltInCommands { - SetContext = 'setContext' -} diff --git a/extensions/azurecore/src/controllers/controllerBase.ts b/extensions/azurecore/src/controllers/controllerBase.ts deleted file mode 100644 index 15e31972b4..0000000000 --- a/extensions/azurecore/src/controllers/controllerBase.ts +++ /dev/null @@ -1,34 +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 vscode from 'vscode'; - -import { AppContext } from '../appContext'; -import { ApiWrapper } from '../apiWrapper'; - -export default abstract class ControllerBase implements vscode.Disposable { - - public constructor(protected appContext: AppContext) { - } - - protected get apiWrapper(): ApiWrapper { - return this.appContext.apiWrapper; - } - - public get extensionContext(): vscode.ExtensionContext { - return this.appContext && this.appContext.extensionContext; - } - - abstract activate(): Promise; - - abstract deactivate(): void; - - public dispose(): void { - this.deactivate(); - } -} - diff --git a/extensions/azurecore/src/controllers/mainController.ts b/extensions/azurecore/src/controllers/mainController.ts deleted file mode 100644 index 7f38fd8667..0000000000 --- a/extensions/azurecore/src/controllers/mainController.ts +++ /dev/null @@ -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 { - 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); - } -} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts deleted file mode 100644 index 9d693b3fec..0000000000 --- a/extensions/azurecore/src/extension.ts +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; - -import MainController from './controllers/mainController'; -import { AppContext } from './appContext'; -import ControllerBase from './controllers/controllerBase'; -import { ApiWrapper } from './apiWrapper'; - -let controllers: ControllerBase[] = []; - -// 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()); - let activations: Promise[] = []; - - // Start the main controller - let mainController = new MainController(appContext); - controllers.push(mainController); - extensionContext.subscriptions.push(mainController); - activations.push(mainController.activate()); - - return Promise.all(activations) - .then((results: boolean[]) => { - for (let result of results) { - if (!result) { - return false; - } - } - return true; - }); -} - -// this method is called when your extension is deactivated -export function deactivate() { - for (let controller of controllers) { - controller.deactivate(); - } -} diff --git a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts deleted file mode 100644 index 9a3cb3f866..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts +++ /dev/null @@ -1,37 +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 vscode from 'vscode'; -import 'mocha'; - -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceAccountNotSignedInTreeNode } from '../../../azureResource/tree/accountNotSignedInTreeNode'; - -describe('AzureResourceAccountNotSignedInTreeNode.info', function(): void { - it('Should be correct.', async function(): Promise { - const label = 'Sign in to Azure ...'; - - const treeNode = new AzureResourceAccountNotSignedInTreeNode(); - - should(treeNode.nodePathValue).equal('message_accountNotSignedIn'); - - const treeItem = await treeNode.getTreeItem(); - should(treeItem.label).equal(label); - should(treeItem.contextValue).equal(AzureResourceItemType.message); - 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'); - - const nodeInfo = treeNode.getNodeInfo(); - should(nodeInfo.isLeaf).true(); - should(nodeInfo.label).equal(label); - should(nodeInfo.nodeType).equal(AzureResourceItemType.message); - should(nodeInfo.iconType).equal(AzureResourceItemType.message); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts deleted file mode 100644 index 27b64e0160..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts +++ /dev/null @@ -1,294 +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, - IAzureResourceSubscriptionService, - IAzureResourceSubscriptionFilterService -} 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'; - -// Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - -let mockCacheService: TypeMoq.IMock; -let mockContextService: TypeMoq.IMock; -let mockCredentialService: TypeMoq.IMock; -let mockSubscriptionService: TypeMoq.IMock; -let mockSubscriptionFilterService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// 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().object; -const mockCredentials = [mockCredential]; - -const mockSubscription1: AzureResourceSubscription = { - id: 'mock_subscription_1', - name: 'mock subscription 1' -}; -const mockSubscription2: AzureResourceSubscription = { - id: 'mock_subscription_2', - name: 'mock subscription 2' -}; -const mockSubscriptions = [mockSubscription1, mockSubscription2]; -const mockFilteredSubscriptions = [mockSubscription1]; - -let mockSubscriptionCache: { subscriptions: { [accountId: string]: AzureResourceSubscription[]} }; - -describe('AzureResourceAccountTreeNode.info', function(): void { - beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); - mockCacheService = TypeMoq.Mock.ofType(); - mockCredentialService = TypeMoq.Mock.ofType(); - mockSubscriptionService = TypeMoq.Mock.ofType(); - mockSubscriptionFilterService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockSubscriptionCache = { subscriptions: {} }; - - mockServicePool.contextService = mockContextService.object; - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.subscriptionService = mockSubscriptionService.object; - mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; - - mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); - mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); - mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); - }); - - it('Should be correct when created.', async function(): Promise { - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); - - const accountTreeNodeId = `account_${mockAccount.key.accountId}`; - const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId})`; - - should(accountTreeNode.nodePathValue).equal(accountTreeNodeId); - - const treeItem = await accountTreeNode.getTreeItem(); - should(treeItem.id).equal(accountTreeNodeId); - should(treeItem.label).equal(accountTreeNodeLabel); - should(treeItem.contextValue).equal(AzureResourceItemType.account); - should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); - - const nodeInfo = accountTreeNode.getNodeInfo(); - should(nodeInfo.label).equal(accountTreeNodeLabel); - should(nodeInfo.isLeaf).false(); - should(nodeInfo.nodeType).equal(AzureResourceItemType.account); - should(nodeInfo.iconType).equal(AzureResourceItemType.account); - }); - - it('Should be correct when there are subscriptions listed.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).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); - - await accountTreeNode.getChildren(); - - const treeItem = await accountTreeNode.getTreeItem(); - should(treeItem.label).equal(accountTreeNodeLabel); - - const nodeInfo = accountTreeNode.getNodeInfo(); - should(nodeInfo.label).equal(accountTreeNodeLabel); - }); - - it('Should be correct when there are subscriptions filtered.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).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); - - await accountTreeNode.getChildren(); - - const treeItem = await accountTreeNode.getTreeItem(); - should(treeItem.label).equal(accountTreeNodeLabel); - - const nodeInfo = accountTreeNode.getNodeInfo(); - should(nodeInfo.label).equal(accountTreeNodeLabel); - }); -}); - -describe('AzureResourceAccountTreeNode.getChildren', function(): void { - beforeEach(() => { - mockCacheService = TypeMoq.Mock.ofType(); - mockCredentialService = TypeMoq.Mock.ofType(); - mockSubscriptionService = TypeMoq.Mock.ofType(); - mockSubscriptionFilterService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockSubscriptionCache = { subscriptions: {} }; - - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.subscriptionService = mockSubscriptionService.object; - mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; - - mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); - mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); - mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); - }); - - it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); - mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); - - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); - - const children = await accountTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); - mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); - mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); - - mockTreeChangeHandler.verify((o) => o.notifyNodeChanged(accountTreeNode), TypeMoq.Times.once()); - - should(accountTreeNode.totalSubscriptionCount).equal(mockSubscriptions.length); - should(accountTreeNode.selectedSubscriptionCount).equal(mockSubscriptions.length); - should(accountTreeNode.isClearingCache).false(); - - 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); - - 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}`); - } - }); - - it('Should load subscriptions from cache when it is not clearing cache.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); - mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); - - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); - - await accountTreeNode.getChildren(); - const children = await accountTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount), 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)); - - should(children.length).equal(mockSubscriptionCache.subscriptions[mockAccount.key.accountId].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}`); - } - }); - - it('Should handle when there is no subscriptions.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(undefined)); - - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); - - const children = await accountTreeNode.getChildren(); - - should(accountTreeNode.totalSubscriptionCount).equal(0); - - 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 Subscriptions found.'); - }); - - it('Should honor subscription filtering.', async function(): Promise { - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); - mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); - - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); - - const children = await accountTreeNode.getChildren(); - - mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); - - should(accountTreeNode.selectedSubscriptionCount).equal(mockFilteredSubscriptions.length); - should(children.length).equal(mockFilteredSubscriptions.length); - - for (let ix = 0; ix < mockFilteredSubscriptions.length; ix++) { - should(children[ix].nodePathValue).equal(`subscription_${mockFilteredSubscriptions[ix].id}`); - } - }); - - it('Should handle errors.', async function(): Promise { - const mockError = 'Test error'; - mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => { throw new Error(mockError); }); - - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); - - const children = await accountTreeNode.getChildren(); - - mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); - mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); - mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); - mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), 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('AzureResourceAccountTreeNode.clearCache', function() : void { - beforeEach(() => { - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - }); - - it('Should clear cache.', async function(): Promise { - const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); - accountTreeNode.clearCache(); - should(accountTreeNode.isClearingCache).true(); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts deleted file mode 100644 index a2e8640265..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts +++ /dev/null @@ -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; -let mockContextService: TypeMoq.IMock; -let mockCredentialService: TypeMoq.IMock; -let mockDatabaseService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// 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().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(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct when created.', async function(): Promise { - 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(); - mockCredentialService = TypeMoq.Mock.ofType(); - mockDatabaseService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockDatabaseContainerCache = { databases: {} }; - - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.databaseService = mockDatabaseService.object; - - mockCredentialService.setup((o) => o.getCredentials(mockAccount)).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 { - 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), 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 { - 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), 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 { - 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 { - 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), 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(); - }); - - it('Should clear cache.', async function(): Promise { - const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - databaseContainerTreeNode.clearCache(); - should(databaseContainerTreeNode.isClearingCache).true(); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts deleted file mode 100644 index 5bbed2f812..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts +++ /dev/null @@ -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; -let mockContextService: TypeMoq.IMock; -let mockCredentialService: TypeMoq.IMock; -let mockDatabaseServerService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// 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().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(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct when created.', async function(): Promise { - 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(); - mockCredentialService = TypeMoq.Mock.ofType(); - mockDatabaseServerService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockDatabaseServerContainerCache = { databaseServers: {} }; - - mockServicePool.cacheService = mockCacheService.object; - mockServicePool.credentialService = mockCredentialService.object; - mockServicePool.databaseServerService = mockDatabaseServerService.object; - - mockCredentialService.setup((o) => o.getCredentials(mockAccount)).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 { - 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), 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 { - 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), 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 { - 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 { - 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), 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(); - }); - - it('Should clear cache.', async function(): Promise { - const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - databaseServerContainerTreeNode.clearCache(); - should(databaseServerContainerTreeNode.isClearingCache).true(); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts deleted file mode 100644 index cef163b670..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts +++ /dev/null @@ -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; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// 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(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct when created.', async function(): Promise { - 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); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts deleted file mode 100644 index a1350771b8..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts +++ /dev/null @@ -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; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// 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(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct.', async function(): Promise { - 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); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts deleted file mode 100644 index ddd867c60f..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts +++ /dev/null @@ -1,41 +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 vscode from 'vscode'; -import 'mocha'; - -import { AzureResourceItemType } from '../../../azureResource/constants'; -import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; - -describe('AzureResourceMessageTreeNode.info', function(): void { - it('Should be correct when created.', async function(): Promise { - const mockMessage = 'Test messagse'; - const treeNode = new AzureResourceMessageTreeNode(mockMessage, undefined); - - should(treeNode.nodePathValue).startWith('message_'); - - const treeItem = await treeNode.getTreeItem(); - should(treeItem.label).equal(mockMessage); - should(treeItem.contextValue).equal(AzureResourceItemType.message); - should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); - - const nodeInfo = treeNode.getNodeInfo(); - should(nodeInfo.isLeaf).true(); - should(nodeInfo.label).equal(mockMessage); - should(nodeInfo.nodeType).equal(AzureResourceItemType.message); - should(nodeInfo.iconType).equal(AzureResourceItemType.message); - }); -}); - -describe('AzureResourceMessageTreeNode.create', function(): void { - it('Should create a message node.', async function(): Promise { - const mockMessage = 'Test messagse'; - const treeNode = AzureResourceMessageTreeNode.create(mockMessage, undefined); - should(treeNode).instanceof(AzureResourceMessageTreeNode); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts deleted file mode 100644 index e55621dfed..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts +++ /dev/null @@ -1,91 +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 { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { IAzureResourceContextService } from '../../../azureResource/interfaces'; -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'; - -// Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - -let mockContextService: TypeMoq.IMock; - -let mockTreeChangeHandler: TypeMoq.IMock; - -// 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: AzureResourceSubscription = { - id: 'mock_subscription', - name: 'mock subscription' -}; - -describe('AzureResourceSubscriptionTreeNode.info', function(): void { - beforeEach(() => { - mockContextService = TypeMoq.Mock.ofType(); - - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - - mockServicePool.contextService = mockContextService.object; - }); - - it('Should be correct when created.', async function(): Promise { - const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - - should(subscriptionTreeNode.nodePathValue).equal(`subscription_${mockSubscription.id}`); - - const treeItem = await subscriptionTreeNode.getTreeItem(); - should(treeItem.label).equal(mockSubscription.name); - should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); - should(treeItem.contextValue).equal(AzureResourceItemType.subscription); - - const nodeInfo = subscriptionTreeNode.getNodeInfo(); - should(nodeInfo.label).equal(mockSubscription.name); - should(nodeInfo.isLeaf).equal(false); - should(nodeInfo.nodeType).equal(AzureResourceItemType.subscription); - should(nodeInfo.iconType).equal(AzureResourceItemType.subscription); - }); -}); - -describe('AzureResourceSubscriptionTreeNode.getChildren', function(): void { - beforeEach(() => { - mockTreeChangeHandler = TypeMoq.Mock.ofType(); - }); - - it('Should load database containers.', async function(): Promise { - const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); - const children = await subscriptionTreeNode.getChildren(); - - should(children).Array(); - should(children.length).equal(2); - should(children[0]).instanceof(AzureResourceDatabaseContainerTreeNode); - should(children[1]).instanceof(AzureResourceDatabaseServerContainerTreeNode); - }); -}); diff --git a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts deleted file mode 100644 index f8cf33c84c..0000000000 --- a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts +++ /dev/null @@ -1,113 +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 'mocha'; - -import { AzureResourceServicePool } from '../../../azureResource/servicePool'; -import { 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'; - -// Mock services -const mockServicePool = AzureResourceServicePool.getInstance(); - -let mockAccountService: TypeMoq.IMock; - -// Mock test data -const mockAccount1: sqlops.Account = { - key: { - accountId: 'mock_account_1', - providerId: 'mock_provider' - }, - displayInfo: { - displayName: 'mock_account_1@test.com', - accountType: 'Microsoft', - contextualDisplayName: 'test' - }, - properties: undefined, - isStale: false -}; -const mockAccount2: sqlops.Account = { - key: { - accountId: 'mock_account_2', - providerId: 'mock_provider' - }, - displayInfo: { - displayName: 'mock_account_2@test.com', - accountType: 'Microsoft', - contextualDisplayName: 'test' - }, - properties: undefined, - isStale: false -}; -const mockAccounts = [mockAccount1, mockAccount2]; - -describe('AzureResourceTreeProvider.getChildren', function(): void { - beforeEach(() => { - mockAccountService = TypeMoq.Mock.ofType(); - - mockServicePool.accountService = mockAccountService.object; - }); - - it('Should load accounts.', async function(): Promise { - mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(mockAccounts)); - - const treeProvider = new AzureResourceTreeProvider(); - treeProvider.isSystemInitialized = true; - - const children = await treeProvider.getChildren(undefined); - - mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); - - should(children).Array(); - should(children.length).equal(mockAccounts.length); - - for (let ix = 0; ix < mockAccounts.length; ix++) { - const child = children[ix]; - const account = mockAccounts[ix]; - - should(child).instanceof(AzureResourceAccountTreeNode); - should(child.nodePathValue).equal(`account_${account.key.accountId}`); - } - }); - - it('Should handle when there is no accounts.', async function(): Promise { - mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(undefined)); - - const treeProvider = new AzureResourceTreeProvider(); - treeProvider.isSystemInitialized = true; - - const children = await treeProvider.getChildren(undefined); - - should(children).Array(); - should(children.length).equal(1); - should(children[0]).instanceof(AzureResourceAccountNotSignedInTreeNode); - }); - - it('Should handle errors.', async function(): Promise { - const mockAccountError = 'Test account error'; - mockAccountService.setup((o) => o.getAccounts()).returns(() => { throw new Error(mockAccountError); }); - - const treeProvider = new AzureResourceTreeProvider(); - treeProvider.isSystemInitialized = true; - - const children = await treeProvider.getChildren(undefined); - - mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); - - 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: ${mockAccountError}`); - }); -}); diff --git a/extensions/azurecore/src/test/index.ts b/extensions/azurecore/src/test/index.ts deleted file mode 100644 index a9f0ffbd82..0000000000 --- a/extensions/azurecore/src/test/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const path = require('path'); -const testRunner = require('vscode/lib/testrunner'); - -const suite = 'Integration Azure Tests'; - -const options: any = { - ui: 'bdd', - useColors: true, - timeout: 60000 -}; - -if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { - options.reporter = 'mocha-multi-reporters'; - options.reporterOptions = { - reporterEnabled: 'spec, mocha-junit-reporter', - mochaJunitReporterReporterOptions: { - testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) - } - }; -} - -testRunner.configure(options); - -export = testRunner; diff --git a/extensions/azurecore/src/treeNodes.ts b/extensions/azurecore/src/treeNodes.ts deleted file mode 100644 index 7136ba98bd..0000000000 --- a/extensions/azurecore/src/treeNodes.ts +++ /dev/null @@ -1,77 +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 * 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) { - path = this.parent.generateNodePath(); - } - path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue; - return path; - } - - public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise { - let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path; - let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath); - return TreeNode.findNode(this, condition, filter, true); - } - - public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise { - if (!node) { - return undefined; - } - - if (condition(node)) { - return node; - } - - let nodeInfo = node.getNodeInfo(); - if (nodeInfo.isLeaf) { - return undefined; - } - - // TODO support filtering by already expanded / not yet expanded - let children = await node.getChildren(false); - if (children) { - for (let child of children) { - if (filter && filter(child)) { - let childNode = await this.findNode(child, condition, filter, expandIfNeeded); - if (childNode) { - return childNode; - } - } - } - } - return undefined; - } - - /** - * The value to use for this node in the node path - */ - public abstract get nodePathValue(): string; - - abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise; - abstract getTreeItem(): vscode.TreeItem | Promise; - - abstract getNodeInfo(): sqlops.NodeInfo; -} diff --git a/extensions/azurecore/src/typings/ref.d.ts b/extensions/azurecore/src/typings/ref.d.ts deleted file mode 100644 index 41e273db7f..0000000000 --- a/extensions/azurecore/src/typings/ref.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/// -/// -/// -/// \ No newline at end of file diff --git a/extensions/azurecore/tsconfig.json b/extensions/azurecore/tsconfig.json deleted file mode 100644 index b341a65dab..0000000000 --- a/extensions/azurecore/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compileOnSave": true, - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": "./out", - "lib": [ - "es6", "es2015.promise" - ], - "typeRoots": [ - "./node_modules/@types" - ], - "sourceMap": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "moduleResolution": "node", - "declaration": true - }, - "exclude": [ - "node_modules" - ] -} diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock deleted file mode 100644 index 8c2535a991..0000000000 --- a/extensions/azurecore/yarn.lock +++ /dev/null @@ -1,608 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/mocha@^5.2.5": - version "5.2.5" - resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" - -"@types/node@^8.0.47": - version "8.10.30" - resolved "https://registry.npmjs.org/@types/node/-/node-8.10.30.tgz#2c82cbed5f79d72280c131d2acffa88fbd8dd353" - -adal-node@^0.1.28: - version "0.1.28" - resolved "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485" - dependencies: - "@types/node" "^8.0.47" - async ">=0.6.0" - date-utils "*" - jws "3.x.x" - request ">= 2.52.0" - underscore ">= 1.3.1" - uuid "^3.1.0" - xmldom ">= 0.1.x" - xpath.js "~1.1.0" - -ajv@^5.3.0: - version "5.5.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -async@2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" - dependencies: - lodash "^4.14.0" - -async@>=0.6.0: - version "2.6.1" - resolved "https://registry.npmjs.org/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - dependencies: - lodash "^4.17.10" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - -azure-arm-resource@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/azure-arm-resource/-/azure-arm-resource-7.0.0.tgz#e76fe2195abe354b607346c2fa0f690544176294" - dependencies: - ms-rest "^2.3.3" - ms-rest-azure "^2.5.5" - -azure-arm-sql@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/azure-arm-sql/-/azure-arm-sql-5.0.1.tgz#75c0b115525d2270ab16122d47d0c666aca175d4" - dependencies: - ms-rest "^2.3.3" - ms-rest-azure "^2.5.5" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - dependencies: - tweetnacl "^0.14.3" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - -circular-json@^0.3.1: - version "0.3.3" - resolved "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -combined-stream@1.0.6: - version "1.0.6" - resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - dependencies: - delayed-stream "~1.0.0" - -combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - dependencies: - delayed-stream "~1.0.0" - -commander@2.15.1: - version "2.15.1" - resolved "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -date-utils@*: - version "1.2.21" - resolved "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" - -debug@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - -duplexer@^0.1.1: - version "0.1.1" - resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ecdsa-sig-formatter@1.0.10: - version "1.0.10" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" - dependencies: - safe-buffer "^5.0.1" - -escape-string-regexp@1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@~2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - dependencies: - asynckit "^0.4.0" - combined-stream "1.0.6" - mime-types "^2.1.12" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - dependencies: - assert-plus "^1.0.0" - -glob@7.1.2: - version "7.1.2" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - -har-validator@~5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" - dependencies: - ajv "^5.3.0" - har-schema "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - -he@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -is-buffer@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -jwa@^1.1.5: - version "1.1.6" - resolved "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.10" - safe-buffer "^5.0.1" - -jws@3.x.x: - version "3.1.5" - resolved "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" - dependencies: - jwa "^1.1.5" - safe-buffer "^5.0.1" - -lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4: - version "4.17.11" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - -mime-db@~1.36.0: - version "1.36.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.20" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" - dependencies: - mime-db "~1.36.0" - -minimatch@3.0.4, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -mkdirp@0.5.1: - version "0.5.1" - resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -mocha@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" - dependencies: - browser-stdout "1.3.1" - commander "2.15.1" - debug "3.1.0" - diff "3.5.0" - escape-string-regexp "1.0.5" - glob "7.1.2" - growl "1.10.5" - he "1.1.1" - minimatch "3.0.4" - mkdirp "0.5.1" - supports-color "5.4.0" - -moment@^2.21.0, moment@^2.22.2: - version "2.22.2" - resolved "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" - -ms-rest-azure@^2.5.5: - version "2.5.9" - resolved "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.5.9.tgz#8599943e349c91eb367d2d1dcb885017518dc712" - dependencies: - adal-node "^0.1.28" - async "2.6.0" - moment "^2.22.2" - ms-rest "^2.3.2" - request "^2.88.0" - uuid "^3.2.1" - -ms-rest@^2.3.2, ms-rest@^2.3.3: - version "2.3.7" - resolved "https://registry.npmjs.org/ms-rest/-/ms-rest-2.3.7.tgz#8bfc82fb91807643fcaa487c5fc9698cd18a018c" - dependencies: - duplexer "^0.1.1" - is-buffer "^1.1.6" - is-stream "^1.1.0" - moment "^2.21.0" - request "^2.88.0" - through "^2.3.8" - tunnel "0.0.5" - uuid "^3.2.1" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - -postinstall-build@^5.0.1: - version "5.0.3" - resolved "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" - -psl@^1.1.24: - version "1.1.29" - resolved "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - -"request@>= 2.52.0", request@^2.88.0: - version "2.88.0" - resolved "https://registry.npmjs.org/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - -should-equal@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" - dependencies: - should-type "^1.4.0" - -should-format@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" - dependencies: - should-type "^1.3.0" - should-type-adaptors "^1.0.1" - -should-type-adaptors@^1.0.1: - version "1.1.0" - resolved "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" - dependencies: - should-type "^1.3.0" - should-util "^1.0.0" - -should-type@^1.3.0, should-type@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" - -should-util@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" - -should@^13.2.1: - version "13.2.3" - resolved "https://registry.npmjs.org/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" - dependencies: - should-equal "^2.0.0" - should-format "^3.0.3" - should-type "^1.4.0" - should-type-adaptors "^1.0.1" - should-util "^1.0.0" - -sshpk@^1.7.0: - version "1.14.2" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - safer-buffer "^2.0.2" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -supports-color@5.4.0: - version "5.4.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" - dependencies: - has-flag "^3.0.0" - -through@^2.3.8: - version "2.3.8" - resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - dependencies: - safe-buffer "^5.0.1" - -tunnel@0.0.5: - version "0.0.5" - resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz#d1532254749ed36620fcd1010865495a1fa9d0ae" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - -typemoq@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" - dependencies: - circular-json "^0.3.1" - lodash "^4.17.4" - postinstall-build "^5.0.1" - -"underscore@>= 1.3.1": - version "1.9.1" - resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" - -uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vscode-nls@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -"xmldom@>= 0.1.x": - version "0.1.27" - resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" - -xpath.js@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 4e613f5592..929b92b9bc 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -6,11 +6,8 @@ pushd %~dp0\.. set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% :: Tests in the extension host -:: TODO port over an re-enable API tests -:: 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-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% 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% . diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index fba91607f9..a48257ad4f 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -13,11 +13,9 @@ fi cd $ROOT # Tests in the extension host -# TODO port over an re-enable API tests -# ./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-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 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 . diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index 41612e8fb2..9b057aa729 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -1349,12 +1349,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti * TODO this could be a map reduce operation */ public buildConnectionInfo(connectionString: string, provider: string): Thenable { - let connectionProvider = this._providers.get(provider); - if (connectionProvider) { - return connectionProvider.onReady.then(e => { - return e.buildConnectionInfo(connectionString); - }); - } - return Promise.resolve(undefined); + return this._providers.get(provider).onReady.then(e => { + return e.buildConnectionInfo(connectionString); + }); } } From 48b899b5d01ba7fa0baeb4f0034b63ed3d533d67 Mon Sep 17 00:00:00 2001 From: anthonypants Date: Fri, 5 Oct 2018 13:02:06 -0700 Subject: [PATCH 13/26] Typo (#2775) Adam Mechanic => Adam Machanic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a179a1dcd6..3f85be0f82 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ We would like to thank all our users who raised issues, and in particular the fo * stebet for `Fix #153: Fixing sql snippets that failed on a DB with case-sensitive collation. (#152)` * SebastianPfliegel `Remove sqlExtensionHelp (#312)` * olljanat for `Implemented npm version check (#314)` -* Adam Mechanic for helping with the `whoisactive` extension +* Adam Machanic for helping with the `whoisactive` extension * All community localization contributors: * French: Adrien Clerbois, ANAS BELABBES, Antoine Griffard, Arian Papillon, Eric Macarez, Eric Van Thorre, Jérémy LANDON, Matthias GROSPERRIN, Maxime COQUEREL, Olivier Guinart, thierry DEMAN-BARCELÒ, Thomas Potier * Italian: Aldo Donetti, Alessandro Alpi, Andrea Dottor, Bruni Luca, Gianluca Hotz, Luca Nardi, Luigi Bruno, Marco Dal Pino, Mirco Vanini, Pasquale Ceglie, Riccardo Cappello, Sergio Govoni, Stefano Demiliani From 8a8745701b711cde08a0f62c4689a5518b99ee76 Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Fri, 5 Oct 2018 14:14:19 -0700 Subject: [PATCH 14/26] Stop OE connection spinner when user closes connection dialog (#2777) --- .../parts/connection/common/connectionManagement.ts | 2 ++ .../connection/common/connectionManagementService.ts | 4 +++- .../connectionDialog/connectionDialogService.ts | 3 +++ src/sql/parts/editData/common/editDataInput.ts | 3 +++ .../parts/objectExplorer/viewlet/treeUpdateUtils.ts | 12 +++++++----- src/sql/parts/query/common/queryInput.ts | 3 +++ .../parts/connection/connectionDialogService.test.ts | 3 ++- .../connection/connectionManagementService.test.ts | 4 ++++ 8 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/sql/parts/connection/common/connectionManagement.ts b/src/sql/parts/connection/common/connectionManagement.ts index 724d735d49..35339f1932 100644 --- a/src/sql/parts/connection/common/connectionManagement.ts +++ b/src/sql/parts/connection/common/connectionManagement.ts @@ -67,6 +67,7 @@ export interface IConnectionCallbacks { onConnectReject(error?: string): void; onConnectSuccess(params?: INewConnectionParams): void; onDisconnect(): void; + onConnectCanceled(): void; } export const SERVICE_ID = 'connectionManagementService'; @@ -336,6 +337,7 @@ export interface IConnectableInput { onConnectReject(error?: string): void; onConnectSuccess(params?: INewConnectionParams): void; onDisconnect(): void; + onConnectCanceled(): void; } export enum ConnectionType { diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index 9b057aa729..af123e2d0c 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -366,6 +366,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti onConnectStart: callbacks ? callbacks.onConnectStart : undefined, onConnectSuccess: callbacks ? callbacks.onConnectSuccess : undefined, onDisconnect: callbacks ? callbacks.onDisconnect : undefined, + onConnectCanceled: callbacks ? callbacks.onConnectCanceled : undefined, uri: uri }; } @@ -438,7 +439,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti onConnectReject: () => { }, onConnectStart: () => { }, onConnectSuccess: () => { }, - onDisconnect: () => { } + onDisconnect: () => { }, + onConnectCanceled: () => { } }; } if (!options) { diff --git a/src/sql/parts/connection/connectionDialog/connectionDialogService.ts b/src/sql/parts/connection/connectionDialog/connectionDialogService.ts index ef88092c4c..e54ae207d4 100644 --- a/src/sql/parts/connection/connectionDialog/connectionDialogService.ts +++ b/src/sql/parts/connection/connectionDialog/connectionDialogService.ts @@ -153,6 +153,9 @@ export class ConnectionDialogService implements IConnectionDialogService { } else { this._connectionManagementService.cancelConnection(this._model); } + if (params && params.input && params.input.onConnectCanceled) { + params.input.onConnectCanceled(); + } this._connectionDialog.resetConnection(); this._connecting = false; } diff --git a/src/sql/parts/editData/common/editDataInput.ts b/src/sql/parts/editData/common/editDataInput.ts index 833337ffec..e69d3e465d 100644 --- a/src/sql/parts/editData/common/editDataInput.ts +++ b/src/sql/parts/editData/common/editDataInput.ts @@ -175,6 +175,9 @@ export class EditDataInput extends EditorInput implements IConnectableInput { } } + public onConnectCanceled(): void { + } + public onConnectSuccess(params?: INewConnectionParams): void { let rowLimit: number = undefined; let queryString: string = undefined; diff --git a/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts b/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts index 7a7f54b18b..332ef93a07 100644 --- a/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts +++ b/src/sql/parts/objectExplorer/viewlet/treeUpdateUtils.ts @@ -130,14 +130,16 @@ export class TreeUpdateUtils { if (tree) { // Show the spinner in OE by adding the 'loading' trait to the connection, and set up callbacks to hide the spinner tree.addTraits('loading', [connection]); + let rejectOrCancelCallback = () => { + tree.collapse(connection); + tree.removeTraits('loading', [connection]); + }; callbacks = { onConnectStart: undefined, - onConnectReject: () => { - tree.collapse(connection); - tree.removeTraits('loading', [connection]); - }, + onConnectReject: rejectOrCancelCallback, onConnectSuccess: () => tree.removeTraits('loading', [connection]), - onDisconnect: undefined + onDisconnect: undefined, + onConnectCanceled: rejectOrCancelCallback, }; } connectionManagementService.connect(connection, undefined, options, callbacks).then(result => { diff --git a/src/sql/parts/query/common/queryInput.ts b/src/sql/parts/query/common/queryInput.ts index bf430b32a0..50ee9f2b94 100644 --- a/src/sql/parts/query/common/queryInput.ts +++ b/src/sql/parts/query/common/queryInput.ts @@ -225,6 +225,9 @@ export class QueryInput extends EditorInput implements IEncodingSupport, IConnec this._updateTaskbar.fire(); } + public onConnectCanceled(): void { + } + public onConnectSuccess(params?: INewConnectionParams): void { this._runQueryEnabled = true; this._connectEnabled = false; diff --git a/src/sqltest/parts/connection/connectionDialogService.test.ts b/src/sqltest/parts/connection/connectionDialogService.test.ts index d04e8504f4..f660f9e7d1 100644 --- a/src/sqltest/parts/connection/connectionDialogService.test.ts +++ b/src/sqltest/parts/connection/connectionDialogService.test.ts @@ -56,7 +56,8 @@ suite('ConnectionDialogService tests', () => { onConnectStart: undefined, onConnectSuccess: undefined, onConnectReject: undefined, - onDisconnect: undefined + onDisconnect: undefined, + onConnectCanceled: undefined }, runQueryOnCompletion: undefined, querySelection: undefined diff --git a/src/sqltest/parts/connection/connectionManagementService.test.ts b/src/sqltest/parts/connection/connectionManagementService.test.ts index 08797fab38..4594b059cd 100644 --- a/src/sqltest/parts/connection/connectionManagementService.test.ts +++ b/src/sqltest/parts/connection/connectionManagementService.test.ts @@ -259,6 +259,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectStart: undefined, onDisconnect: undefined, onConnectSuccess: undefined, + onConnectCanceled: undefined, uri: 'Editor Uri' }, runQueryOnCompletion: RunQueryOnConnectionMode.executeQuery @@ -279,6 +280,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectStart: undefined, onDisconnect: undefined, onConnectSuccess: undefined, + onConnectCanceled: undefined, uri: 'Editor Uri' }, runQueryOnCompletion: RunQueryOnConnectionMode.executeQuery @@ -348,6 +350,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectReject: undefined, onConnectStart: undefined, onDisconnect: undefined, + onConnectCanceled: undefined, uri: uri }, querySelection: undefined, @@ -661,6 +664,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectReject: undefined, onConnectStart: undefined, onDisconnect: undefined, + onConnectCanceled: undefined, uri: uri }, querySelection: undefined, From dad831bc8ce608df3997cd4ee25b2aaae0cd18c2 Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Mon, 8 Oct 2018 11:16:11 -0700 Subject: [PATCH 15/26] Fix bug where failed OE expands could not be retried (#2780) --- .../common/objectExplorerService.ts | 4 +++- .../connection/objectExplorerService.test.ts | 17 +++++++++++++++++ src/vs/base/parts/tree/browser/treeModel.ts | 6 ------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/sql/parts/objectExplorer/common/objectExplorerService.ts b/src/sql/parts/objectExplorer/common/objectExplorerService.ts index 0dd0847354..46ab83a7f1 100644 --- a/src/sql/parts/objectExplorer/common/objectExplorerService.ts +++ b/src/sql/parts/objectExplorer/common/objectExplorerService.ts @@ -350,7 +350,9 @@ export class ObjectExplorerService implements IObjectExplorerService { } public resolveTreeNodeChildren(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable { - return this.expandOrRefreshTreeNode(session, parentTree); + // Always refresh the node if it has an error, otherwise expand it normally + let needsRefresh = !!parentTree.errorStateMessage; + return this.expandOrRefreshTreeNode(session, parentTree, needsRefresh); } public refreshTreeNode(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable { diff --git a/src/sqltest/parts/connection/objectExplorerService.test.ts b/src/sqltest/parts/connection/objectExplorerService.test.ts index db324406ec..6641083cea 100644 --- a/src/sqltest/parts/connection/objectExplorerService.test.ts +++ b/src/sqltest/parts/connection/objectExplorerService.test.ts @@ -753,4 +753,21 @@ suite('SQL Object Explorer Service tests', () => { assert.equal(childNode.nodePath, objectExplorerExpandInfoRefresh.nodes[index].nodePath); }); }); + + test('resolveTreeNodeChildren refreshes a node if it currently has an error', async () => { + await objectExplorerService.createNewSession('MSSQL', connection); + objectExplorerService.onSessionCreated(1, objectExplorerSession); + + // If I call resolveTreeNodeChildren once, set an error on the node, and then call it again + let tablesNodePath = 'testServerName/tables'; + let tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, tablesNodePath, '', '', null, null, undefined, undefined); + tablesNode.connection = connection; + await objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, tablesNode); + sqlOEProvider.verify(x => x.refreshNode(TypeMoq.It.is(x => x.nodePath === tablesNodePath)), TypeMoq.Times.never()); + tablesNode.errorStateMessage = 'test error message'; + await objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, tablesNode); + + // Then refresh gets called on the node + sqlOEProvider.verify(x => x.refreshNode(TypeMoq.It.is(x => x.nodePath === tablesNodePath)), TypeMoq.Times.once()); + }); }); \ No newline at end of file diff --git a/src/vs/base/parts/tree/browser/treeModel.ts b/src/vs/base/parts/tree/browser/treeModel.ts index 3d5600b732..6bef5e9f71 100644 --- a/src/vs/base/parts/tree/browser/treeModel.ts +++ b/src/vs/base/parts/tree/browser/treeModel.ts @@ -361,12 +361,6 @@ export class Item { return WinJS.TPromise.as(false); } - // {{SQL CARBON EDIT}} - Original code does not handle the need to refresh children in case previous refreshchildren errored out. - // resetting the errorStateMessage before we refresh children so we can track if there has been error in processing. - if (this.element instanceof TreeNode) { - this.element.errorStateMessage = null; - } - var result = this.lock.run(this, () => { var eventData: IItemExpandEvent = { item: this }; var result: WinJS.Promise; From bd39468b96050f8705585e8b020c2d5b999a71cb Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Mon, 8 Oct 2018 13:49:08 -0700 Subject: [PATCH 16/26] Bump SQL Tools Service to 1.5.0-alpha.41 --- extensions/mssql/src/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index c12fe99b36..27028603aa 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "1.5.0-alpha.34", + "version": "1.5.0-alpha.41", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp2.1.zip", "Windows_64": "win-x64-netcoreapp2.1.zip", From 65cc585697e726cbef2f9edd04156dd810583257 Mon Sep 17 00:00:00 2001 From: Keith Stolte Date: Mon, 8 Oct 2018 16:57:54 -0400 Subject: [PATCH 17/26] Fixing a reference to SQL Ops Studio (#2788) :memo: (Contributing) Fixing a reference to SQL ops studio to match new branding of Azure Data Studio. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df1ec0efa4..5912694ee1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ The more information you can provide, the more likely someone will be successful Please include the following with each issue. -* Version of SQL Ops Studio +* Version of Azure Data Studio (formerly SQL Operations Studio). > **Tip:** You can easily create an issue using `Report Issues` from Azure Data Studio Help menu. From 70141bd049083e9f237206be65201c72b809bd26 Mon Sep 17 00:00:00 2001 From: Keith Stolte Date: Mon, 8 Oct 2018 16:58:18 -0400 Subject: [PATCH 18/26] Fix Invalid Configuration in Launch.json (#2789) --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d3e8009964..d8788d99cb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -148,7 +148,7 @@ { "name": "Renderer and Extension Host processes", "configurations": [ - "Launch SQL Ops", + "Launch azuredatastudio", "Attach to Extension Host" ] } From 0bd179c6cadeaa5cb1a78abe8ff626a323b5608b Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Mon, 8 Oct 2018 15:41:55 -0700 Subject: [PATCH 19/26] Fix bug where connections edited in connection dialog moved to bottom of group (#2782) --- src/sql/parts/connection/common/connectionConfig.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sql/parts/connection/common/connectionConfig.ts b/src/sql/parts/connection/common/connectionConfig.ts index 83a858c86e..5d94f9dd85 100644 --- a/src/sql/parts/connection/common/connectionConfig.ts +++ b/src/sql/parts/connection/common/connectionConfig.ts @@ -84,13 +84,14 @@ export class ConnectionConfig implements IConnectionConfig { return providerConnectionProfile.matches(connectionProfile); }); if (sameProfileInList) { - profiles = profiles.filter(value => value !== sameProfileInList); + let profileIndex = profiles.findIndex(value => value === sameProfileInList); newProfile.id = sameProfileInList.id; connectionProfile.id = sameProfileInList.id; + profiles[profileIndex] = newProfile; + } else { + profiles.push(newProfile); } - profiles.push(newProfile); - this.writeConfiguration(Constants.connectionsArrayName, profiles).then(() => { resolve(connectionProfile); }).catch(err => { From 4b79ecc3d93655afe37a3f2d03f0c1d60c513b34 Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Mon, 8 Oct 2018 15:42:13 -0700 Subject: [PATCH 20/26] Fix bug disconnecting during stuck OE operation (#2773) --- .../common/objectExplorerService.ts | 22 +++++++++++-- .../objectExplorer/viewlet/serverTreeView.ts | 7 +++-- .../connection/objectExplorerService.test.ts | 31 ++++++++++++++++--- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/sql/parts/objectExplorer/common/objectExplorerService.ts b/src/sql/parts/objectExplorer/common/objectExplorerService.ts index 46ab83a7f1..8680e0b864 100644 --- a/src/sql/parts/objectExplorer/common/objectExplorerService.ts +++ b/src/sql/parts/objectExplorer/common/objectExplorerService.ts @@ -53,7 +53,7 @@ export interface IObjectExplorerService { updateObjectExplorerNodes(connectionProfile: IConnectionProfile): Promise; - deleteObjectExplorerNode(connection: IConnectionProfile): void; + deleteObjectExplorerNode(connection: IConnectionProfile): Thenable; onUpdateObjectExplorerNodes: Event; @@ -142,16 +142,17 @@ export class ObjectExplorerService implements IObjectExplorerService { }); } - public deleteObjectExplorerNode(connection: IConnectionProfile): void { + public deleteObjectExplorerNode(connection: IConnectionProfile): Thenable { let self = this; var connectionUri = connection.id; var nodeTree = this._activeObjectExplorerNodes[connectionUri]; if (nodeTree) { - self.closeSession(connection.providerName, nodeTree.getSession()).then(() => { + return self.closeSession(connection.providerName, nodeTree.getSession()).then(() => { delete self._activeObjectExplorerNodes[connectionUri]; delete self._sessions[nodeTree.getSession().sessionId]; }); } + return Promise.resolve(); } /** @@ -328,6 +329,21 @@ export class ObjectExplorerService implements IObjectExplorerService { } public closeSession(providerId: string, session: sqlops.ObjectExplorerSession): Thenable { + // Complete any requests that are still open for the session + let sessionStatus = this._sessions[session.sessionId]; + if (sessionStatus && sessionStatus.nodes) { + Object.entries(sessionStatus.nodes).forEach(([nodePath, nodeStatus]: [string, NodeStatus]) => { + if (nodeStatus.expandEmitter) { + nodeStatus.expandEmitter.fire({ + sessionId: session.sessionId, + nodes: [], + nodePath: nodePath, + errorMessage: undefined + }); + } + }); + } + let provider = this._providers[providerId]; if (provider) { return provider.closeSession({ diff --git a/src/sql/parts/objectExplorer/viewlet/serverTreeView.ts b/src/sql/parts/objectExplorer/viewlet/serverTreeView.ts index cbc77e8ce4..38c9dc49b5 100644 --- a/src/sql/parts/objectExplorer/viewlet/serverTreeView.ts +++ b/src/sql/parts/objectExplorer/viewlet/serverTreeView.ts @@ -231,9 +231,10 @@ export class ServerTreeView { if (connection) { var conn = this.getConnectionInTreeInput(connection.id); if (conn) { - this._objectExplorerService.deleteObjectExplorerNode(conn); - this._tree.collapse(conn); - this._tree.refresh(conn); + this._objectExplorerService.deleteObjectExplorerNode(conn).then(() => { + this._tree.collapse(conn); + this._tree.refresh(conn); + }); } } } diff --git a/src/sqltest/parts/connection/objectExplorerService.test.ts b/src/sqltest/parts/connection/objectExplorerService.test.ts index 6641083cea..67cc030fb5 100644 --- a/src/sqltest/parts/connection/objectExplorerService.test.ts +++ b/src/sqltest/parts/connection/objectExplorerService.test.ts @@ -447,10 +447,11 @@ suite('SQL Object Explorer Service tests', () => { objectExplorerService.updateObjectExplorerNodes(connection).then(() => { var treeNode = objectExplorerService.getObjectExplorerNode(connection); assert.equal(treeNode !== null && treeNode !== undefined, true); - objectExplorerService.deleteObjectExplorerNode(connection); - treeNode = objectExplorerService.getObjectExplorerNode(connection); - assert.equal(treeNode === null || treeNode === undefined, true); - done(); + objectExplorerService.deleteObjectExplorerNode(connection).then(() => { + treeNode = objectExplorerService.getObjectExplorerNode(connection); + assert.equal(treeNode === null || treeNode === undefined, true); + done(); + }); }, err => { // Must call done here so test indicates it's finished if errors occur done(err); @@ -754,6 +755,28 @@ suite('SQL Object Explorer Service tests', () => { }); }); + test('Session can be closed even if expand requests are pending', async () => { + const providerId = 'MSSQL'; + + // Set up the session + await objectExplorerService.createNewSession(providerId, connection); + objectExplorerService.onSessionCreated(1, objectExplorerSession); + + // Set up the provider to not respond to the second expand request, simulating a request that takes a long time to complete + const nodePath = objectExplorerSession.rootNode.nodePath; + sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.is(x => x.nodePath === nodePath))).callback(() => { }).returns(() => TPromise.as(true)); + + // If I queue a second expand request (the first completes normally because of the original mock) and then close the session + await objectExplorerService.expandNode(providerId, objectExplorerSession, objectExplorerSession.rootNode.nodePath); + let expandPromise = objectExplorerService.expandNode(providerId, objectExplorerSession, objectExplorerSession.rootNode.nodePath); + let closeSessionResult = await objectExplorerService.closeSession(providerId, objectExplorerSession); + + // Then the expand request has completed and the session is closed + let expandResult = await expandPromise; + assert.equal(expandResult.nodes.length, 0); + assert.equal(closeSessionResult.success, true); + }); + test('resolveTreeNodeChildren refreshes a node if it currently has an error', async () => { await objectExplorerService.createNewSession('MSSQL', connection); objectExplorerService.onSessionCreated(1, objectExplorerSession); From 933aa88dc7592fe71b415e71cf6135129f5721a6 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Mon, 8 Oct 2018 17:24:40 -0700 Subject: [PATCH 21/26] Account for Horizontal Scrolling in Grid (#2774) * implement horizontal scroll login in the grid plugin * remove commented code * formatting --- .../plugins/mousewheelTableScroll.plugin.ts | 85 ++++++++++++++----- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts b/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts index f45bd29b99..0e363938c0 100644 --- a/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts @@ -6,6 +6,7 @@ 'use strict'; import * as DOM from 'vs/base/browser/dom'; +import * as Platform from 'vs/base/common/platform'; import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; @@ -29,7 +30,7 @@ export class MouseWheelSupport implements Slick.Plugin { private _disposables: IDisposable[] = []; constructor(options: IMouseWheelSupportOptions = {}) { - this.options = mixin(options, defaultOptions); + this.options = mixin(options, defaultOptions, false); } public init(grid: Slick.Grid): void { @@ -43,29 +44,69 @@ export class MouseWheelSupport implements Slick.Plugin { this._disposables.push(DOM.addDisposableListener(this.viewport, 'DOMMouseScroll', onMouseWheel)); } - private _onMouseWheel(event: StandardMouseWheelEvent) { - const scrollHeight = this.canvas.clientHeight; - const height = this.viewport.clientHeight; - const scrollDown = Math.sign(event.deltaY) === -1; - if (scrollDown) { - if ((this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed)) + height > scrollHeight) { - this.viewport.scrollTop = scrollHeight - height; - this.viewport.dispatchEvent(new Event('scroll')); - } else { - this.viewport.scrollTop = this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed); - this.viewport.dispatchEvent(new Event('scroll')); - event.stopPropagation(); - event.preventDefault(); + private _onMouseWheel(e: StandardMouseWheelEvent) { + if (e.deltaY || e.deltaX) { + let deltaY = e.deltaY * this.options.scrollSpeed; + let deltaX = e.deltaX * this.options.scrollSpeed; + const scrollHeight = this.canvas.clientHeight; + const scrollWidth = this.canvas.clientWidth; + const height = this.viewport.clientHeight; + const width = this.viewport.clientWidth; + + // Convert vertical scrolling to horizontal if shift is held, this + // is handled at a higher level on Mac + const shiftConvert = !Platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey; + if (shiftConvert && !deltaX) { + deltaX = deltaY; + deltaY = 0; } - } else { - if ((this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed)) < 0) { - this.viewport.scrollTop = 0; - this.viewport.dispatchEvent(new Event('scroll')); + + // scroll down + if (deltaY < 0) { + if ((this.viewport.scrollTop - deltaY) + height > scrollHeight) { + this.viewport.scrollTop = scrollHeight - height; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollTop = this.viewport.scrollTop - deltaY; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } + // scroll up } else { - this.viewport.scrollTop = this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed); - this.viewport.dispatchEvent(new Event('scroll')); - event.stopPropagation(); - event.preventDefault(); + if ((this.viewport.scrollTop - deltaY) < 0) { + this.viewport.scrollTop = 0; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollTop = this.viewport.scrollTop - deltaY; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } + } + + // scroll left + if (deltaX < 0) { + if ((this.viewport.scrollLeft - deltaX) + width > scrollWidth) { + this.viewport.scrollLeft = scrollWidth - width; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollLeft = this.viewport.scrollLeft - deltaX; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } + // scroll left + } else { + if ((this.viewport.scrollLeft - deltaX) < 0) { + this.viewport.scrollLeft = 0; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollLeft = this.viewport.scrollLeft - deltaX; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } } } } From 7aa2ee08bf3af8bb2a096fcf0865909b29da81c0 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Mon, 8 Oct 2018 17:30:08 -0700 Subject: [PATCH 22/26] properly reset to handle maximized grids (#2786) --- src/sql/parts/query/editor/gridPanel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sql/parts/query/editor/gridPanel.ts b/src/sql/parts/query/editor/gridPanel.ts index 12bd059aa5..f6ba04b042 100644 --- a/src/sql/parts/query/editor/gridPanel.ts +++ b/src/sql/parts/query/editor/gridPanel.ts @@ -242,6 +242,7 @@ export class GridPanel extends ViewletPanel { } dispose(this.tables); this.tables = []; + this.maximizedGrid = undefined; this.maximumBodySize = this.tables.reduce((p, c) => { return p + c.maximumSize; From c800e70ec128f698fb3c3f20eb1fea0f64a95f3b Mon Sep 17 00:00:00 2001 From: Aditya Bist Date: Tue, 9 Oct 2018 10:28:55 -0700 Subject: [PATCH 23/26] Agent - Step Actions (#2779) * fixed right click context menu bug in jobs view * added stepInfo and edit job WIP * show jobs in job edit * added schedule description on select schedule * fetch schedules during history and show in edit job * added alerts to job histories and show in edit * made history calls async * filter menus now close when esc is pressed * fixed bug where clicking on error row wouldnt populate job details * added functionality to delete steps in a job * added real time adding steps in edit job --- extensions/agent/src/data/jobStepData.ts | 55 ++++++++++++++----- extensions/agent/src/dialogs/agentDialog.ts | 6 +- extensions/agent/src/dialogs/jobDialog.ts | 24 ++++++-- extensions/agent/src/dialogs/jobStepDialog.ts | 30 +++++----- .../agent/src/dialogs/pickScheduleDialog.ts | 1 - extensions/agent/src/interfaces.ts | 2 +- extensions/agent/src/mainController.ts | 11 ++-- .../parts/jobManagement/common/interfaces.ts | 2 + .../parts/jobManagement/common/jobActions.ts | 43 +++++++++++++++ .../common/jobManagementService.ts | 36 ++++++++---- .../views/jobHistory.component.ts | 1 - .../views/jobStepsView.component.ts | 6 +- .../jobManagement/views/jobStepsViewTree.ts | 10 ---- .../jobManagement/views/jobsView.component.ts | 19 ++++++- src/sql/sqlops.d.ts | 6 +- .../workbench/api/node/extHostDataProtocol.ts | 7 +++ .../api/node/mainThreadDataProtocol.ts | 3 + .../workbench/api/node/sqlExtHost.protocol.ts | 5 ++ 18 files changed, 190 insertions(+), 77 deletions(-) diff --git a/extensions/agent/src/data/jobStepData.ts b/extensions/agent/src/data/jobStepData.ts index e11ec015c8..79efe4bd39 100644 --- a/extensions/agent/src/data/jobStepData.ts +++ b/extensions/agent/src/data/jobStepData.ts @@ -6,37 +6,47 @@ import { AgentUtils } from '../agentUtils'; import { IAgentDialogData, AgentDialogMode } from '../interfaces'; +import { JobData } from './jobData'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); export class JobStepData implements IAgentDialogData { + + // Error Messages + private readonly CreateStepErrorMessage_JobNameIsEmpty = localize('stepData.jobNameRequired', 'Job name must be provided'); + private readonly CreateStepErrorMessage_StepNameIsEmpty = localize('stepData.stepNameRequired', 'Step name must be provided'); + public dialogMode: AgentDialogMode = AgentDialogMode.CREATE; public ownerUri: string; - public jobId: string; // + public jobId: string; public jobName: string; - public script: string; // + public script: string; public scriptName: string; - public stepName: string; // - public subSystem: string; // + public stepName: string; + public subSystem: string; public id: number; - public failureAction: string; // - public successAction: string; // + public failureAction: string; + public successAction: string; public failStepId: number; public successStepId: number; public command: string; public commandExecutionSuccessCode: number; - public databaseName: string; // + public databaseName: string; public databaseUserName: string; public server: string; - public outputFileName: string; // + public outputFileName: string; public appendToLogFile: boolean; public appendToStepHist: boolean; public writeLogToTable: boolean; public appendLogToTable: boolean; - public retryAttempts: number; // - public retryInterval: number; // + public retryAttempts: number; + public retryInterval: number; public proxyName: string; - constructor(ownerUri:string) { + constructor(ownerUri:string, jobModel?: JobData) { this.ownerUri = ownerUri; + this.jobName = jobModel.name; } public async initialize() { @@ -51,7 +61,7 @@ export class JobStepData implements IAgentDialogData { scriptName: this.scriptName, stepName: this.stepName, subSystem: this.subSystem, - id: 1, + id: this.id, failureAction: this.failureAction, successAction: this.successAction, failStepId: this.failStepId, @@ -70,7 +80,26 @@ export class JobStepData implements IAgentDialogData { retryInterval: this.retryInterval, proxyName: this.proxyName }).then(result => { - console.info(result); + if (result && result.success) { + console.info(result); + } }); } + + public validate(): { valid: boolean, errorMessages: string[] } { + let validationErrors: string[] = []; + + if (!(this.stepName && this.stepName.trim())) { + validationErrors.push(this.CreateStepErrorMessage_StepNameIsEmpty); + } + + if (!(this.jobName && this.jobName.trim())) { + validationErrors.push(this.CreateStepErrorMessage_JobNameIsEmpty); + } + + return { + valid: validationErrors.length === 0, + errorMessages: validationErrors + }; + } } \ No newline at end of file diff --git a/extensions/agent/src/dialogs/agentDialog.ts b/extensions/agent/src/dialogs/agentDialog.ts index fa2c1cea85..63d5d240d2 100644 --- a/extensions/agent/src/dialogs/agentDialog.ts +++ b/extensions/agent/src/dialogs/agentDialog.ts @@ -49,10 +49,8 @@ export abstract class AgentDialog { protected async execute() { this.updateModel(); - let success = await this.model.save(); - if (success) { - this._onSuccess.fire(this.model); - } + await this.model.save(); + this._onSuccess.fire(this.model); } protected async cancel() { diff --git a/extensions/agent/src/dialogs/jobDialog.ts b/extensions/agent/src/dialogs/jobDialog.ts index ab86a4f699..8bd76509ee 100644 --- a/extensions/agent/src/dialogs/jobDialog.ts +++ b/extensions/agent/src/dialogs/jobDialog.ts @@ -10,6 +10,7 @@ import { JobStepDialog } from './jobStepDialog'; import { PickScheduleDialog } from './pickScheduleDialog'; import { AlertDialog } from './alertDialog'; import { AgentDialog } from './agentDialog'; +import { AgentUtils } from '../agentUtils'; const localize = nls.loadMessageBundle(); @@ -234,10 +235,14 @@ export class JobDialog extends AgentDialog { width: 80 }).component(); + let stepDialog = new JobStepDialog(this.model.ownerUri, '' , data.length + 1, this.model); + stepDialog.onSuccess((step) => { + this.model.jobSteps.push(step); + this.stepsTable.data = this.convertStepsToData(this.model.jobSteps); + }); this.newStepButton.onDidClick((e)=>{ if (this.nameTextBox.value && this.nameTextBox.value.length > 0) { - let stepDialog = new JobStepDialog(this.model.ownerUri, this.nameTextBox.value, '' , 1, this.model); - stepDialog.openNewStepDialog(); + stepDialog.openDialog(); } else { this.dialog.message = { text: this.BlankJobNameErrorText }; } @@ -273,9 +278,17 @@ export class JobDialog extends AgentDialog { }); this.deleteStepButton.onDidClick((e) => { - // implement delete steps - - + AgentUtils.getAgentService().then((agentService) => { + let steps = this.model.jobSteps ? this.model.jobSteps : []; + agentService.deleteJobStep(this.ownerUri, stepData).then((result) => { + if (result && result.success) { + delete steps[rowNumber]; + this.model.jobSteps = steps; + let data = this.convertStepsToData(steps); + this.stepsTable.data = data; + } + }); + }); }); } }); @@ -529,7 +542,6 @@ export class JobDialog extends AgentDialog { let result = []; alerts.forEach(alert => { let cols = []; - console.log(alert); cols.push(alert.name); cols.push(alert.isEnabled); cols.push(alert.alertType.toString()); diff --git a/extensions/agent/src/dialogs/jobStepDialog.ts b/extensions/agent/src/dialogs/jobStepDialog.ts index a7be6423ab..737851d448 100644 --- a/extensions/agent/src/dialogs/jobStepDialog.ts +++ b/extensions/agent/src/dialogs/jobStepDialog.ts @@ -9,11 +9,12 @@ import * as vscode from 'vscode'; import { JobStepData } from '../data/jobStepData'; import { AgentUtils } from '../agentUtils'; import { JobData } from '../data/jobData'; +import { AgentDialog } from './agentDialog'; const path = require('path'); const localize = nls.loadMessageBundle(); -export class JobStepDialog { +export class JobStepDialog extends AgentDialog { // TODO: localize // Top level @@ -67,7 +68,6 @@ export class JobStepDialog { // UI Components // Dialogs - private dialog: sqlops.window.modelviewdialog.Dialog; private fileBrowserDialog: sqlops.window.modelviewdialog.Dialog; // Dialog tabs @@ -105,35 +105,27 @@ export class JobStepDialog { private fileBrowserTree: sqlops.FileBrowserTreeComponent; private jobModel: JobData; - private model: JobStepData; - private ownerUri: string; private jobName: string; private server: string; private stepId: number; constructor( ownerUri: string, - jobName: string, server: string, stepId: number, jobModel?: JobData ) { - this.model = new JobStepData(ownerUri); + super(ownerUri, new JobStepData(ownerUri, jobModel), 'New Step'); this.stepId = stepId; - this.ownerUri = ownerUri; - this.jobName = jobName; + this.jobName = jobModel.name; this.server = server; this.jobModel = jobModel; } private initializeUIComponents() { - this.dialog = sqlops.window.modelviewdialog.createDialog(this.DialogTitle); this.generalTab = sqlops.window.modelviewdialog.createTab(this.GeneralTabText); this.advancedTab = sqlops.window.modelviewdialog.createTab(this.AdvancedTabText); this.dialog.content = [this.generalTab, this.advancedTab]; - this.dialog.okButton.onClick(async () => await this.execute()); - this.dialog.okButton.label = this.OkButtonText; - this.dialog.cancelButton.label = this.CancelButtonText; } private createCommands(view, queryProvider: sqlops.QueryProvider) { @@ -478,7 +470,7 @@ export class JobStepDialog { return outputFileForm; } - protected execute() { + protected updateModel() { this.model.stepName = this.nameTextBox.value; if (!this.model.stepName || this.model.stepName.length === 0) { this.dialog.message = this.dialog.message = { text: this.BlankStepNameErrorText }; @@ -499,12 +491,20 @@ export class JobStepDialog { this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked; } - public async openNewStepDialog() { + public async initializeDialog() { let databases = await AgentUtils.getDatabases(this.ownerUri); let queryProvider = await AgentUtils.getQueryProvider(); this.initializeUIComponents(); this.createGeneralTab(databases, queryProvider); this.createAdvancedTab(); - sqlops.window.modelviewdialog.openDialog(this.dialog); + this.dialog.registerCloseValidator(() => { + this.updateModel(); + let validationResult = this.model.validate(); + if (!validationResult.valid) { + // TODO: Show Error Messages + console.error(validationResult.errorMessages.join(',')); + } + return validationResult.valid; + }); } } \ No newline at end of file diff --git a/extensions/agent/src/dialogs/pickScheduleDialog.ts b/extensions/agent/src/dialogs/pickScheduleDialog.ts index 76e17e94a3..5fcd1ac1d1 100644 --- a/extensions/agent/src/dialogs/pickScheduleDialog.ts +++ b/extensions/agent/src/dialogs/pickScheduleDialog.ts @@ -74,7 +74,6 @@ export class PickScheduleDialog { let data: any[][] = []; for (let i = 0; i < this.model.schedules.length; ++i) { let schedule = this.model.schedules[i]; - console.log(schedule); data[i] = [ schedule.id, schedule.name, schedule.description ]; } this.schedulesTable.data = data; diff --git a/extensions/agent/src/interfaces.ts b/extensions/agent/src/interfaces.ts index 1ea6f4dc05..0e86394477 100644 --- a/extensions/agent/src/interfaces.ts +++ b/extensions/agent/src/interfaces.ts @@ -13,5 +13,5 @@ export enum AgentDialogMode { export interface IAgentDialogData { dialogMode: AgentDialogMode; initialize(): void; - save(): void; + save(): Promise; } diff --git a/extensions/agent/src/mainController.ts b/extensions/agent/src/mainController.ts index e0d1eedb6a..2797e4512c 100644 --- a/extensions/agent/src/mainController.ts +++ b/extensions/agent/src/mainController.ts @@ -40,9 +40,9 @@ export class MainController { let dialog = new JobDialog(ownerUri, jobInfo); dialog.openDialog(); }); - vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, jobId: string, server: string, stepId: number) => { - let dialog = new JobStepDialog(ownerUri, jobId, server, stepId); - dialog.openNewStepDialog(); + vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, server: string, stepId: number) => { + let dialog = new JobStepDialog(ownerUri, server, stepId); + dialog.openDialog(); }); vscode.commands.registerCommand('agent.openPickScheduleDialog', (ownerUri: string) => { let dialog = new PickScheduleDialog(ownerUri); @@ -57,9 +57,8 @@ export class MainController { dialog.openDialog(); }); vscode.commands.registerCommand('agent.openProxyDialog', (ownerUri: string, proxyInfo: sqlops.AgentProxyInfo, credentials: sqlops.CredentialInfo[]) => { - //@TODO: reenable create proxy after snapping July release (7/14/18) - // let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials); - // dialog.openDialog(); + let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials); + dialog.openDialog(); MainController.showNotYetImplemented(); }); } diff --git a/src/sql/parts/jobManagement/common/interfaces.ts b/src/sql/parts/jobManagement/common/interfaces.ts index f347da987e..2074d9454a 100644 --- a/src/sql/parts/jobManagement/common/interfaces.ts +++ b/src/sql/parts/jobManagement/common/interfaces.ts @@ -25,6 +25,8 @@ export interface IJobManagementService { getJobHistory(connectionUri: string, jobID: string): Thenable; deleteJob(connectionUri: string, job: sqlops.AgentJobInfo): Thenable; + deleteJobStep(connectionUri: string, step: sqlops.AgentJobStepInfo): Thenable; + getAlerts(connectionUri: string): Thenable; deleteAlert(connectionUri: string, alert: sqlops.AgentAlertInfo): Thenable; diff --git a/src/sql/parts/jobManagement/common/jobActions.ts b/src/sql/parts/jobManagement/common/jobActions.ts index fbd8b21317..ef48595e69 100644 --- a/src/sql/parts/jobManagement/common/jobActions.ts +++ b/src/sql/parts/jobManagement/common/jobActions.ts @@ -26,6 +26,7 @@ export enum JobActions { export interface IJobActionInfo { ownerUri: string; targetObject: any; + jobHistoryComponent?: JobHistoryComponent; } // Job actions @@ -230,6 +231,48 @@ export class NewStepAction extends Action { } } +export class DeleteStepAction extends Action { + public static ID = 'jobaction.deleteStep'; + public static LABEL = nls.localize('jobaction.deleteStep', "Delete Step"); + + constructor( + @INotificationService private _notificationService: INotificationService, + @IJobManagementService private _jobService: IJobManagementService, + @IInstantiationService private instantationService: IInstantiationService + ) { + super(DeleteStepAction.ID, DeleteStepAction.LABEL); + } + + public run(actionInfo: IJobActionInfo): TPromise { + let self = this; + let step = actionInfo.targetObject as sqlops.AgentJobStepInfo; + let refreshAction = this.instantationService.createInstance(JobsRefreshAction); + self._notificationService.prompt( + Severity.Info, + nls.localize('jobaction.deleteStepConfirm,', "Are you sure you'd like to delete the step '{0}'?", step.stepName), + [{ + label: DeleteStepAction.LABEL, + run: () => { + self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => { + if (!result || !result.success) { + let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}", + step.stepName, result.errorMessage ? result.errorMessage : 'Unknown error'); + self._notificationService.error(errorMessage); + } else { + refreshAction.run(actionInfo.jobHistoryComponent); + } + }); + } + }, { + label: DeleteAlertAction.CancelLabel, + run: () => { } + }] + ); + return TPromise.as(true); + } +} + + // Alert Actions export class NewAlertAction extends Action { diff --git a/src/sql/parts/jobManagement/common/jobManagementService.ts b/src/sql/parts/jobManagement/common/jobManagementService.ts index 401c487a5f..eaa4ceea2e 100644 --- a/src/sql/parts/jobManagement/common/jobManagementService.ts +++ b/src/sql/parts/jobManagement/common/jobManagementService.ts @@ -30,6 +30,7 @@ export class JobManagementService implements IJobManagementService { this._onDidChange.fire(void 0); } + // Jobs public getJobs(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getJobs(connectionUri); @@ -42,6 +43,27 @@ export class JobManagementService implements IJobManagementService { }); } + public getJobHistory(connectionUri: string, jobID: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.getJobHistory(connectionUri, jobID); + }); + } + + public jobAction(connectionUri: string, jobName: string, action: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.jobAction(connectionUri, jobName, action); + }); + } + + // Steps + public deleteJobStep(connectionUri: string, stepInfo: sqlops.AgentJobStepInfo): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.deleteJobStep(connectionUri, stepInfo); + }); + } + + + // Alerts public getAlerts(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getAlerts(connectionUri); @@ -54,6 +76,7 @@ export class JobManagementService implements IJobManagementService { }); } + // Operators public getOperators(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getOperators(connectionUri); @@ -66,6 +89,7 @@ export class JobManagementService implements IJobManagementService { }); } + // Proxies public getProxies(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getProxies(connectionUri); @@ -84,18 +108,6 @@ export class JobManagementService implements IJobManagementService { }); } - public getJobHistory(connectionUri: string, jobID: string): Thenable { - return this._runAction(connectionUri, (runner) => { - return runner.getJobHistory(connectionUri, jobID); - }); - } - - public jobAction(connectionUri: string, jobName: string, action: string): Thenable { - return this._runAction(connectionUri, (runner) => { - return runner.jobAction(connectionUri, jobName, action); - }); - } - private _runAction(uri: string, action: (handler: sqlops.AgentServicesProvider) => Thenable): Thenable { let providerId: string = this._connectionService.getProviderIdFromUri(uri); diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index fc5971f574..7fe8d1e5ab 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -74,7 +74,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { @Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface, @Inject(forwardRef(() => AgentViewComponent)) private _agentViewComponent: AgentViewComponent, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, - @Inject(INotificationService) private _notificationService: INotificationService, @Inject(IInstantiationService) private instantiationService: IInstantiationService, @Inject(IContextMenuService) private contextMenuService: IContextMenuService, @Inject(IJobManagementService) private _jobManagementService: IJobManagementService, diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.ts b/src/sql/parts/jobManagement/views/jobStepsView.component.ts index 495e62abe8..7a92babbc0 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TabChild } from 'sql/base/browser/ui/panel/tab.component'; -import * as dom from 'vs/base/browser/dom'; +import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces'; export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component'; @@ -36,7 +36,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, private _treeDataSource = new JobStepsViewDataSource(); private _treeRenderer = new JobStepsViewRenderer(); private _treeFilter = new JobStepsViewFilter(); - private static _pageSize = 1024; + private _pageSize = 1024; @ViewChild('table') private _tableContainer: ElementRef; @@ -66,7 +66,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, }, { verticalScrollMode: ScrollbarVisibility.Visible }); this._register(attachListStyler(this._tree, this.themeService)); } - this._tree.layout(500); + this._tree.layout(this._pageSize); this._tree.setInput(new JobStepsViewModel()); $('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1'); $('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0'); diff --git a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts index 49c021f4cd..ac03318b61 100644 --- a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts +++ b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts @@ -10,7 +10,6 @@ import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { generateUuid } from 'vs/base/common/uuid'; -import { AgentJobHistoryInfo } from 'sqlops'; import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities'; export class JobStepsViewRow { @@ -27,7 +26,6 @@ export class JobStepsViewModel { } export class JobStepsViewController extends TreeDefaults.DefaultController { - private _jobHistories: AgentJobHistoryInfo[]; protected onLeftClick(tree: tree.ITree, element: JobStepsViewRow, event: IMouseEvent, origin: string = 'mouse'): boolean { return true; @@ -37,14 +35,6 @@ export class JobStepsViewController extends TreeDefaults.DefaultController { return true; } - public set jobHistories(value: AgentJobHistoryInfo[]) { - this._jobHistories = value; - } - - public get jobHistories(): AgentJobHistoryInfo[] { - return this._jobHistories; - } - } export class JobStepsViewDataSource implements tree.IDataSource { diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index 6625051cad..a6646460ca 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -584,7 +584,12 @@ export class JobsViewComponent extends JobManagementView implements OnInit { self.jobHistories[job.jobId] = result.jobs; self._jobCacheObject.setJobHistory(job.jobId, result.jobs); let jobHistories = self._jobCacheObject.getJobHistory(job.jobId); - let previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length); + let previousRuns: sqlops.AgentJobHistoryInfo[]; + if (jobHistories.length >= 5) { + previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length); + } else { + previousRuns = jobHistories; + } self.createJobChart(job.jobId, previousRuns); if (self._agentViewComponent.expanded.has(job.jobId)) { let lastJobHistory = jobHistories[result.jobs.length - 1]; @@ -645,12 +650,22 @@ export class JobsViewComponent extends JobManagementView implements OnInit { maxDuration = maxDuration === 0 ? 1 : maxDuration; let maxBarHeight: number = 24; let chartHeights = []; + let zeroDurationJobCount = 0; for (let i = 0; i < jobHistories.length; i++) { let duration = jobHistories[i].runDuration; let chartHeight = (maxBarHeight * JobManagementUtilities.convertDurationToSeconds(duration)) / maxDuration; chartHeights.push(`${chartHeight}px`); + if (chartHeight === 0) { + zeroDurationJobCount++; + } + } + // if the durations are all 0 secs, show minimal chart + // instead of nothing + if (zeroDurationJobCount === jobHistories.length) { + return ['5px', '5px', '5px', '5px', '5px']; + } else { + return chartHeights; } - return chartHeights; } private expandJobs(start: boolean): void { diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 401cc534e4..a34e97b8dc 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -1537,9 +1537,9 @@ declare module 'sqlops' { getJobDefaults(ownerUri: string): Thenable; // Job Step management methods - createJobStep(ownerUri: string, jobInfo: AgentJobStepInfo): Thenable; - updateJobStep(ownerUri: string, originalJobStepName: string, jobInfo: AgentJobStepInfo): Thenable; - deleteJobStep(ownerUri: string, jobInfo: AgentJobStepInfo): Thenable; + createJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable; + updateJobStep(ownerUri: string, originalJobStepName: string, stepInfo: AgentJobStepInfo): Thenable; + deleteJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable; // Alert management methods getAlerts(ownerUri: string): Thenable; diff --git a/src/sql/workbench/api/node/extHostDataProtocol.ts b/src/sql/workbench/api/node/extHostDataProtocol.ts index 95bd9d950a..0d430c383e 100644 --- a/src/sql/workbench/api/node/extHostDataProtocol.ts +++ b/src/sql/workbench/api/node/extHostDataProtocol.ts @@ -596,6 +596,13 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { throw this._resolveProvider(handle).deleteJob(ownerUri, job); } + /** + * Deletes a job step + */ + $deleteJobStep(handle: number, ownerUri: string, step: sqlops.AgentJobStepInfo): Thenable { + throw this._resolveProvider(handle).deleteJobStep(ownerUri, step); + } + /** * Get Agent Alerts list */ diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index ba8fa88f2e..bed01823a2 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -359,6 +359,9 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { deleteJob(connectionUri: string, jobInfo: sqlops.AgentJobInfo): Thenable { return self._proxy.$deleteJob(handle, connectionUri, jobInfo); }, + deleteJobStep(connectionUri: string, stepInfo: sqlops.AgentJobStepInfo): Thenable { + return self._proxy.$deleteJobStep(handle, connectionUri, stepInfo); + }, getAlerts(connectionUri: string): Thenable { return self._proxy.$getAlerts(handle, connectionUri); }, diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 77ce5b268c..7b7d94bda3 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -370,6 +370,11 @@ export abstract class ExtHostDataProtocolShape { */ $deleteJob(handle: number, ownerUri: string, job: sqlops.AgentJobInfo): Thenable { throw ni(); } + /** + * Deletes a job step + */ + $deleteJobStep(handle: number, ownerUri: string, step: sqlops.AgentJobStepInfo): Thenable { throw ni(); } + /** * Get Agent Alerts list */ From 34f6811eea6c3315fec8d43d1dce476766445a04 Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Tue, 9 Oct 2018 11:12:12 -0700 Subject: [PATCH 24/26] Bring back Connection Config tests (#2795) --- .../parts/connection/connectionConfig.test.ts | 1761 +++++++++-------- 1 file changed, 891 insertions(+), 870 deletions(-) diff --git a/src/sqltest/parts/connection/connectionConfig.test.ts b/src/sqltest/parts/connection/connectionConfig.test.ts index 4f00287202..b190847e04 100644 --- a/src/sqltest/parts/connection/connectionConfig.test.ts +++ b/src/sqltest/parts/connection/connectionConfig.test.ts @@ -12,29 +12,88 @@ import { IConnectionProfile, IConnectionProfileStore } from 'sql/parts/connectio import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { WorkspaceConfigurationTestService } from 'sqltest/stubs/workspaceConfigurationTestService'; -import { IConfigurationValue as TConfigurationValue, ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; +import { IConfigurationValue, ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; import * as Constants from 'sql/parts/connection/common/constants'; import { IConnectionProfileGroup, ConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup'; import { TPromise } from 'vs/base/common/winjs.base'; import * as assert from 'assert'; -import { CapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; +import { ProviderFeatures, ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; import * as sqlops from 'sqlops'; import { Emitter } from 'vs/base/common/event'; +import { ConnectionOptionSpecialType, ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { StorageService, InMemoryLocalStorage } from 'vs/platform/storage/common/storageService'; +import { CapabilitiesTestService } from 'sqltest/stubs/capabilitiesTestService'; suite('SQL ConnectionConfig tests', () => { - /* - let capabilitiesService: TypeMoq.Mock; - let workspaceConfigurationServiceMock: TypeMoq.Mock; - let configEditingServiceMock: TypeMoq.Mock; - let msSQLCapabilities: sqlops.DataProtocolServerCapabilities; - let capabilities: sqlops.DataProtocolServerCapabilities[]; - let onProviderRegistered = new Emitter(); - - let configValueToConcat: TConfigurationValue = { - workspace: [{ + let capabilitiesService: TypeMoq.Mock; + let workspaceConfigurationServiceMock: TypeMoq.Mock; + let configEditingServiceMock: TypeMoq.Mock; + let msSQLCapabilities: ProviderFeatures; + let capabilities: ProviderFeatures[]; + let onCapabilitiesRegistered = new Emitter(); + + let configValueToConcat = { + workspace: [{ + name: 'g1', + id: 'g1', + parentId: 'ROOT', + color: 'pink', + description: 'g1' + }, + { + name: 'g1-1', + id: 'g1-1', + parentId: 'g1', + color: 'blue', + description: 'g1-1' + } + ], + user: [{ + name: 'ROOT', + id: 'ROOT', + parentId: '', + color: 'white', + description: 'ROOT' + }, { + name: 'g2', + id: 'g2', + parentId: 'ROOT', + color: 'green', + description: 'g2' + }, + { + name: 'g2-1', + id: 'g2-1', + parentId: 'g2', + color: 'yellow', + description: 'g2' + }, + { + name: 'g3', + id: 'g3', + parentId: '', + color: 'orange', + description: 'g3' + }, + { + name: 'g3-1', + id: 'g3-1', + parentId: 'g3', + color: 'purple', + description: 'g3-1' + } + ], + value: [], + default: [], + workspaceFolder: [] + }; + + let configValueToMerge = { + workspace: [ + { name: 'g1', id: 'g1', - parentId: 'ROOT', + parentId: '', color: 'pink', description: 'g1' }, @@ -45,17 +104,12 @@ suite('SQL ConnectionConfig tests', () => { color: 'blue', description: 'g1-1' } - ], - user: [{ - name: 'ROOT', - id: 'ROOT', - parentId: '', - color: 'white', - description: 'ROOT' - }, { + ], + user: [ + { name: 'g2', id: 'g2', - parentId: 'ROOT', + parentId: '', color: 'green', description: 'g2' }, @@ -67,871 +121,838 @@ suite('SQL ConnectionConfig tests', () => { description: 'g2' }, { - name: 'g3', - id: 'g3', + name: 'g1', + id: 'g1', parentId: '', - color: 'orange', - description: 'g3' + color: 'pink', + description: 'g1' }, { - name: 'g3-1', - id: 'g3-1', - parentId: 'g3', - color: 'purple', - description: 'g3-1' - } - ], - value: [], - default: [], - folder: [] - }; - - let configValueToMerge: TConfigurationValue = { - workspace: [ - { - name: 'g1', - id: 'g1', - parentId: '', - color: 'pink', - description: 'g1' - }, - { - name: 'g1-1', - id: 'g1-1', - parentId: 'g1', - color: 'blue', - description: 'g1-1' - } - ], - user: [ - { - name: 'g2', - id: 'g2', - parentId: '', - color: 'green', - description: 'g2' - }, - { - name: 'g2-1', - id: 'g2-1', - parentId: 'g2', - color: 'yellow', - description: 'g2' - }, - { - name: 'g1', - id: 'g1', - parentId: '', - color: 'pink', - description: 'g1' - }, - { - name: 'g1-2', - id: 'g1-2', - parentId: 'g1', - color: 'silver', - description: 'g1-2' - }], - value: [], - default: [], - folder: [] - }; - - let connections: TConfigurationValue = { - workspace: [{ - options: { - serverName: 'server1', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '' - }, - providerName: 'MSSQL', - groupId: 'test', - savePassword: true, - id: 'server1' - } - - - ], - user: [{ - options: { - serverName: 'server2', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '' - }, - providerName: 'MSSQL', - groupId: 'test', - savePassword: true, - id: 'server2' - }, { - options: { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '' - }, - providerName: 'MSSQL', - groupId: 'g3', - savePassword: true, - id: 'server3' - } - ], - value: [], - default: [], - folder: [] - }; - setup(() => { - capabilitiesService = TypeMoq.Mock.ofType(CapabilitiesService); - capabilities = []; - let connectionProvider: sqlops.ConnectionProviderOptions = { - options: [ - { - name: 'serverName', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 0, - valueType: 0 - }, - { - name: 'databaseName', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 1, - valueType: 0 - }, - { - name: 'userName', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 3, - valueType: 0 - }, - { - name: 'authenticationType', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 2, - valueType: 0 - }, - { - name: 'password', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 4, - valueType: 0 - } - ] - }; - msSQLCapabilities = { - protocolVersion: '1', - providerName: 'MSSQL', - providerDisplayName: 'MSSQL', - connectionProvider: connectionProvider, - adminServicesProvider: undefined, - features: undefined - }; - capabilities.push(msSQLCapabilities); - - capabilitiesService.setup(x => x.getCapabilities()).returns(() => capabilities); - capabilitiesService.setup(x => x.onProviderRegisteredEvent).returns(() => onProviderRegistered.event); - - workspaceConfigurationServiceMock = TypeMoq.Mock.ofType(WorkspaceConfigurationTestService); - workspaceConfigurationServiceMock.setup(x => x.reloadConfiguration()) - .returns(() => TPromise.as<{}>({})); - - configEditingServiceMock = TypeMoq.Mock.ofType(ConfigurationEditingService); - let nothing: void; - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); - }); - - function groupsAreEqual(groups1: IConnectionProfileGroup[], groups2: IConnectionProfileGroup[]): Boolean { - if (!groups1 && !groups2) { - return true; - } else if ((!groups1 && groups2 && groups2.length === 0) || (!groups2 && groups1 && groups1.length === 0)) { - return true; - } - - if (groups1.length !== groups2.length) { - return false; - } - - let areEqual = true; - - groups1.map(g1 => { - if (areEqual) { - let g2 = groups2.find(g => g.name === g1.name); - if (!g2) { - areEqual = false; - } else { - let result = groupsAreEqual(groups1.filter(a => a.parentId === g1.id), groups2.filter(b => b.parentId === g2.id)); - if (result === false) { - areEqual = false; - } - } - } - }); - - return areEqual; + name: 'g1-2', + id: 'g1-2', + parentId: 'g1', + color: 'silver', + description: 'g1-2' + }], + value: [], + default: [], + workspaceFolder: [] + }; + + let connections = { + workspace: [{ + options: { + serverName: 'server1', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '' + }, + providerName: 'MSSQL', + groupId: 'test', + savePassword: true, + id: 'server1' } - - test('allGroups should return groups from user and workspace settings', () => { - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allGroups = config.getAllGroups(); - - - assert.notEqual(allGroups, undefined); - assert.equal(allGroups.length, configValueToConcat.workspace.length + configValueToConcat.user.length); - }); - - test('allGroups should merge groups from user and workspace settings', () => { - let expectedAllGroups: IConnectionProfileGroup[] = [ - { - name: 'g1', - id: 'g1', - parentId: '', - color: 'pink', - description: 'g1' - }, - { - name: 'g1-1', - id: 'g1-1', - parentId: 'g1', - color: 'blue', - description: 'g1-1' - }, - { - name: 'g2', - id: 'g2', - parentId: '', - color: 'yellow', - description: 'g2' - }, - { - name: 'g2-1', - id: 'g2-1', - parentId: 'g2', - color: 'red', - description: 'g2' - }, - { - name: 'g1-2', - id: 'g1-2', - parentId: 'g1', - color: 'green', - description: 'g1-2' - }]; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToMerge); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allGroups = config.getAllGroups(); - - - assert.notEqual(allGroups, undefined); - assert.equal(groupsAreEqual(allGroups, expectedAllGroups), true); - }); - - test('addConnection should add the new profile to user settings if does not exist', done => { - let newProfile: IConnectionProfile = { - serverName: 'new server', + + + ], + user: [{ + options: { + serverName: 'server2', databaseName: 'database', userName: 'user', password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: undefined, - groupId: undefined, - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length + 1; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = 'database'; - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.addConnection(connectionProfile).then(savedConnectionProfile => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - assert.notEqual(savedConnectionProfile.id, undefined); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('addConnection should not add the new profile to user settings if already exists', done => { - let profileFromConfig = connections.user[0]; - let newProfile: IConnectionProfile = { - serverName: profileFromConfig.options['serverName'], - databaseName: profileFromConfig.options['databaseName'], - userName: profileFromConfig.options['userName'], - password: profileFromConfig.options['password'], - authenticationType: profileFromConfig.options['authenticationType'], - groupId: profileFromConfig.groupId, - savePassword: true, - groupFullName: undefined, - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = profileFromConfig.options['databaseName']; - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.addConnection(connectionProfile).then(savedConnectionProfile => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - assert.equal(savedConnectionProfile.id, profileFromConfig.id); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('addConnection should add the new group to user settings if does not exist', done => { - let newProfile: IConnectionProfile = { - serverName: 'new server', + authenticationType: '' + }, + providerName: 'MSSQL', + groupId: 'test', + savePassword: true, + id: 'server2' + }, { + options: { + serverName: 'server3', databaseName: 'database', userName: 'user', password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g2/g2-2', - groupId: undefined, - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length + 1; - let expectedNumberOfGroups = configValueToConcat.user.length + 1; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.addConnection(connectionProfile).then(success => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('getConnections should return connections from user and workspace settings given getWorkspaceConnections set to true', () => { - let getWorkspaceConnections: boolean = true; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allConnections = config.getConnections(getWorkspaceConnections); - assert.equal(allConnections.length, connections.user.length + connections.workspace.length); - }); - - test('getConnections should return connections from user settings given getWorkspaceConnections set to false', () => { - let getWorkspaceConnections: boolean = false; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allConnections = config.getConnections(getWorkspaceConnections); - assert.equal(allConnections.length, connections.user.length); - }); - - test('getConnections should return connections with a valid id', () => { - let getWorkspaceConnections: boolean = false; - let connectionsWithNoId: TConfigurationValue = { - user: connections.user.map(c => { - c.id = undefined; - return c; - }), - default: connections.default, - workspace: connections.workspace.map(c => { - c.id = c.options['serverName']; - return c; - }), - value: connections.value, - folder: [] - }; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connectionsWithNoId); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allConnections = config.getConnections(getWorkspaceConnections); - assert.equal(allConnections.length, connections.user.length); - allConnections.forEach(connection => { - let userConnection = connectionsWithNoId.user.find(u => u.options['serverName'] === connection.serverName); - if (userConnection !== undefined) { - assert.notEqual(connection.id, connection.getOptionsKey()); - assert.notEqual(connection.id, undefined); - } else { - let workspaceConnection = connectionsWithNoId.workspace.find(u => u.options['serverName'] === connection.serverName); - assert.notEqual(connection.id, connection.getOptionsKey()); - assert.equal(workspaceConnection.id, connection.id); + authenticationType: '' + }, + providerName: 'MSSQL', + groupId: 'g3', + savePassword: true, + id: 'server3' + } + ], + value: [], + default: [], + workspaceFolder: [] + }; + setup(() => { + capabilitiesService = TypeMoq.Mock.ofType(CapabilitiesTestService); + capabilities = []; + let connectionProvider: sqlops.ConnectionProviderOptions = { + options: [ + { + name: 'serverName', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.serverName, + valueType: ServiceOptionType.string + }, + { + name: 'databaseName', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.databaseName, + valueType: ServiceOptionType.string + }, + { + name: 'userName', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.userName, + valueType: ServiceOptionType.string + }, + { + name: 'authenticationType', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.authType, + valueType: ServiceOptionType.string + }, + { + name: 'password', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.password, + valueType: ServiceOptionType.string + } + ] + }; + msSQLCapabilities = { + connection: { + providerId: 'MSSQL', + displayName: 'MSSQL', + connectionOptions: connectionProvider.options + } + }; + capabilities.push(msSQLCapabilities); + + capabilitiesService.setup(x => x.getCapabilities('MSSQL')).returns(() => msSQLCapabilities); + (capabilitiesService.object as any).onCapabilitiesRegistered = onCapabilitiesRegistered.event; + + workspaceConfigurationServiceMock = TypeMoq.Mock.ofType(WorkspaceConfigurationTestService); + workspaceConfigurationServiceMock.setup(x => x.reloadConfiguration()) + .returns(() => TPromise.as(null)); + + configEditingServiceMock = TypeMoq.Mock.ofType(ConfigurationEditingService); + let nothing: void; + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); + }); + + function groupsAreEqual(groups1: IConnectionProfileGroup[], groups2: IConnectionProfileGroup[]): Boolean { + if (!groups1 && !groups2) { + return true; + } else if ((!groups1 && groups2 && groups2.length === 0) || (!groups2 && groups1 && groups1.length === 0)) { + return true; + } + + if (groups1.length !== groups2.length) { + return false; + } + + let areEqual = true; + + groups1.map(g1 => { + if (areEqual) { + let g2 = groups2.find(g => g.name === g1.name); + if (!g2) { + areEqual = false; + } else { + let result = groupsAreEqual(groups1.filter(a => a.parentId === g1.id), groups2.filter(b => b.parentId === g2.id)); + if (result === false) { + areEqual = false; + } } - }); - }); - - test('getConnections update the capabilities in each profile when the provider capabilities is registered', () => { - let oldOptionName: string = 'oldOptionName'; - let optionsMetadataFromConfig = capabilities[0].connectionProvider.options.concat({ - name: oldOptionName, - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 0, - valueType: 0 - }); - - let capabilitiesFromConfig: sqlops.DataProtocolServerCapabilities[] = []; - let connectionProvider: sqlops.ConnectionProviderOptions = { - options: optionsMetadataFromConfig - }; - let msSQLCapabilities2 = { - protocolVersion: '1', - providerName: 'MSSQL', - providerDisplayName: 'MSSQL', - connectionProvider: connectionProvider, - adminServicesProvider: undefined, - features: undefined - }; - capabilitiesFromConfig.push(msSQLCapabilities2); - let connectionUsingOldMetadata = connections.user.map(c => { - c.options[oldOptionName] = 'oldOptionValue'; - return c; - }); - let configValue = Object.assign({}, connections, { user: connectionUsingOldMetadata }); - let capabilitiesService2: TypeMoq.Mock = TypeMoq.Mock.ofType(CapabilitiesService); - capabilitiesService2.setup(x => x.getCapabilities()).returns(() => []); - capabilitiesService2.setup(x => x.onProviderRegisteredEvent).returns(() => onProviderRegistered.event); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => configValue); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService2.object, capabilitiesFromConfig); - let allConnections = config.getConnections(false); - allConnections.forEach(element => { - assert.notEqual(element.serverName, undefined); - assert.notEqual(element.getOptionsKey().indexOf('oldOptionValue|'), -1); - }); - - onProviderRegistered.fire(msSQLCapabilities); - allConnections.forEach(element => { - assert.notEqual(element.serverName, undefined); - assert.equal(element.getOptionsKey().indexOf('oldOptionValue|'), -1); - }); - }); - - test('saveGroup should save the new groups to tree and return the id of the last group name', () => { - let config = new ConnectionConfig(undefined, undefined, undefined, undefined); - let groups: IConnectionProfileGroup[] = configValueToConcat.user; - let expectedLength = configValueToConcat.user.length + 2; - let newGroups: string = 'ROOT/g1/g1-1'; - let color: string = 'red'; - - let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); - assert.notEqual(result, undefined); - assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); - let newGroup = result.groups.find(g => g.name === 'g1-1'); - assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); - }); - - test('saveGroup should only add the groups that are not in the tree', () => { - let config = new ConnectionConfig(undefined, undefined, undefined, undefined); - let groups: IConnectionProfileGroup[] = configValueToConcat.user; - let expectedLength = configValueToConcat.user.length + 1; - let newGroups: string = 'ROOT/g2/g2-5'; - let color: string = 'red'; - - let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); - assert.notEqual(result, undefined); - assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); - let newGroup = result.groups.find(g => g.name === 'g2-5'); - assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); - }); - - test('saveGroup should not add any new group if tree already has all the groups in the full path', () => { - let config = new ConnectionConfig(undefined, undefined, undefined, undefined); - let groups: IConnectionProfileGroup[] = configValueToConcat.user; - let expectedLength = configValueToConcat.user.length; - let newGroups: string = 'ROOT/g2/g2-1'; - let color: string = 'red'; - - let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); - assert.notEqual(result, undefined); - assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); - let newGroup = result.groups.find(g => g.name === 'g2-1'); - assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); - }); - - test('deleteConnection should remove the connection from config', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'g3', - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length - 1; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = 'database'; - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.deleteConnection(connectionProfile).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('deleteConnectionGroup should remove the children connections and subgroups from config', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'g3', - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = 'database'; - - let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); - let childGroup = new ConnectionProfileGroup('g3-1', connectionProfileGroup, 'g3-1', undefined, undefined); - connectionProfileGroup.addGroups([childGroup]); - connectionProfileGroup.addConnections([connectionProfile]); - - let expectedNumberOfConnections = connections.user.length - 1; - let expectedNumberOfGroups = configValueToConcat.user.length - 2; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.deleteGroup(connectionProfileGroup).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('deleteConnection should not throw error for connection not in config', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'newid', - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.deleteConnection(connectionProfile).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('renameGroup should change group name', done => { - - let expectedNumberOfConnections = configValueToConcat.user.length; - let calledValue: any; - let called: boolean = false; - let nothing: void; - let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { - calledValue = val.value as IConnectionProfileStore[]; - }).returns(() => TPromise.as(nothing)); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfileGroup = new ConnectionProfileGroup('g-renamed', undefined, 'g2', undefined, undefined); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.editGroup(connectionProfileGroup).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - calledValue.forEach(con => { - if (con.id === 'g2') { - assert.equal(con.name, 'g-renamed', 'Group was not renamed'); - called = true; - } - }); - assert.equal(called, true, 'group was not renamed'); - }).then(() => done(), (err) => done(err)); - }); - - - test('change group(parent) for connection group', done => { - let expectedNumberOfConnections = configValueToConcat.user.length; - let calledValue: any; - let nothing: void; - let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { - calledValue = val.value as IConnectionProfileStore[]; - }).returns(() => TPromise.as(nothing)); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let sourceProfileGroup = new ConnectionProfileGroup('g2', undefined, 'g2', undefined, undefined); - let targetProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.changeGroupIdForConnectionGroup(sourceProfileGroup, targetProfileGroup).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - calledValue.forEach(con => { - if (con.id === 'g2') { - assert.equal(con.parentId, 'g3', 'Group parent was not changed'); - } - }); - }).then(() => done(), (err) => done(err)); - }); - - - test('change group(parent) for connection', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'g3', - getOptionsKey: () => { return 'connectionId'; }, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: 'test' - }; - - let expectedNumberOfConnections = connections.user.length; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - let newId = 'newid'; - let calledValue: any; - let nothing: void; - let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { - calledValue = val.value as IConnectionProfileStore[]; - }).returns(() => TPromise.as(nothing)); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).callback((x: any, val: any) => { - - }).returns(() => TPromise.as(nothing)); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.changeGroupIdForConnection(connectionProfile, newId).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.atLeastOnce()); - calledValue.forEach(con => { - }); - }).then(() => done(), (err) => done(err)); - }); - - test('fixConnectionIds should replace duplicate ids with new ones', (done) => { - let profiles: IConnectionProfileStore[] = [ - { - options: {}, - groupId: '1', - id: '1', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '2', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '3', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '2', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '4', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '3', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '2', - providerName: undefined, - savePassword: true, - } - ]; - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.fixConnectionIds(profiles); - let ids = profiles.map(x => x.id); - for (var index = 0; index < ids.length; index++) { - var id = ids[index]; - assert.equal(ids.lastIndexOf(id), index); } - done(); }); - */ + + return areEqual; + } + + test('allGroups should return groups from user and workspace settings', () => { + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allGroups = config.getAllGroups(); + + + assert.notEqual(allGroups, undefined); + assert.equal(allGroups.length, configValueToConcat.workspace.length + configValueToConcat.user.length); + }); + + test('allGroups should merge groups from user and workspace settings', () => { + let expectedAllGroups: IConnectionProfileGroup[] = [ + { + name: 'g1', + id: 'g1', + parentId: '', + color: 'pink', + description: 'g1' + }, + { + name: 'g1-1', + id: 'g1-1', + parentId: 'g1', + color: 'blue', + description: 'g1-1' + }, + { + name: 'g2', + id: 'g2', + parentId: '', + color: 'yellow', + description: 'g2' + }, + { + name: 'g2-1', + id: 'g2-1', + parentId: 'g2', + color: 'red', + description: 'g2' + }, + { + name: 'g1-2', + id: 'g1-2', + parentId: 'g1', + color: 'green', + description: 'g1-2' + }]; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToMerge); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allGroups = config.getAllGroups(); + + + assert.notEqual(allGroups, undefined); + assert.equal(groupsAreEqual(allGroups, expectedAllGroups), true); + }); + + test('addConnection should add the new profile to user settings if does not exist', done => { + let newProfile: IConnectionProfile = { + serverName: 'new server', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: undefined, + groupId: undefined, + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length + 1; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = 'database'; + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.addConnection(connectionProfile).then(savedConnectionProfile => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + assert.notEqual(savedConnectionProfile.id, undefined); + done(); + }).catch(error => { + done(error); + }); + }); + + test('addConnection should not add the new profile to user settings if already exists', done => { + let profileFromConfig = connections.user[0]; + let newProfile: IConnectionProfile = { + serverName: profileFromConfig.options['serverName'], + databaseName: profileFromConfig.options['databaseName'], + userName: profileFromConfig.options['userName'], + password: profileFromConfig.options['password'], + authenticationType: profileFromConfig.options['authenticationType'], + groupId: profileFromConfig.groupId, + savePassword: true, + groupFullName: undefined, + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = profileFromConfig.options['databaseName']; + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.addConnection(connectionProfile).then(savedConnectionProfile => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + assert.equal(savedConnectionProfile.id, profileFromConfig.id); + done(); + }).catch(error => { + done(error); + }); + }); + + test('addConnection should add the new group to user settings if does not exist', done => { + let newProfile: IConnectionProfile = { + serverName: 'new server', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g2/g2-2', + groupId: undefined, + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length + 1; + let expectedNumberOfGroups = configValueToConcat.user.length + 1; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.addConnection(connectionProfile).then(success => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('getConnections should return connections from user and workspace settings given getWorkspaceConnections set to true', () => { + let getWorkspaceConnections: boolean = true; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allConnections = config.getConnections(getWorkspaceConnections); + assert.equal(allConnections.length, connections.user.length + connections.workspace.length); + }); + + test('getConnections should return connections from user settings given getWorkspaceConnections set to false', () => { + let getWorkspaceConnections: boolean = false; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allConnections = config.getConnections(getWorkspaceConnections); + assert.equal(allConnections.length, connections.user.length); + }); + + test('getConnections should return connections with a valid id', () => { + let getWorkspaceConnections: boolean = false; + let connectionsWithNoId = { + user: connections.user.map(c => { + c.id = undefined; + return c; + }), + default: connections.default, + workspace: connections.workspace.map(c => { + c.id = c.options['serverName']; + return c; + }), + value: connections.value, + workspaceFolder: [] + }; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connectionsWithNoId); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allConnections = config.getConnections(getWorkspaceConnections); + assert.equal(allConnections.length, connections.user.length); + allConnections.forEach(connection => { + let userConnection = connectionsWithNoId.user.find(u => u.options['serverName'] === connection.serverName); + if (userConnection !== undefined) { + assert.notEqual(connection.id, connection.getOptionsKey()); + assert.notEqual(connection.id, undefined); + } else { + let workspaceConnection = connectionsWithNoId.workspace.find(u => u.options['serverName'] === connection.serverName); + assert.notEqual(connection.id, connection.getOptionsKey()); + assert.equal(workspaceConnection.id, connection.id); + } + }); + }); + + test('getConnections update the capabilities in each profile when the provider capabilities is registered', () => { + let oldOptionName: string = 'oldOptionName'; + let optionsMetadataFromConfig = capabilities[0].connection.connectionOptions.concat({ + name: oldOptionName, + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: undefined, + valueType: ServiceOptionType.string + }); + + let capabilitiesFromConfig: ProviderFeatures[] = []; + let msSQLCapabilities2: ProviderFeatures = { + connection: { + providerId: 'MSSQL', + displayName: 'MSSQL', + connectionOptions: optionsMetadataFromConfig + } + }; + capabilitiesFromConfig.push(msSQLCapabilities2); + let connectionUsingOldMetadata = connections.user.map(c => { + c.options[oldOptionName] = 'oldOptionValue'; + return c; + }); + let configValue = Object.assign({}, connections, { user: connectionUsingOldMetadata }); + let capabilitiesService2: TypeMoq.Mock = TypeMoq.Mock.ofType(CapabilitiesTestService); + capabilitiesService2.setup(x => x.getCapabilities('MSSQL')).returns(() => msSQLCapabilities2); + (capabilitiesService2.object as any).onCapabilitiesRegistered = onCapabilitiesRegistered.event; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => configValue); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService2.object); + let allConnections = config.getConnections(false); + allConnections.forEach(element => { + assert.notEqual(element.serverName, undefined); + assert.notEqual(element.getOptionsKey().indexOf('oldOptionValue|'), -1); + }); + + onCapabilitiesRegistered.fire(msSQLCapabilities); + allConnections.forEach(element => { + assert.notEqual(element.serverName, undefined); + assert.equal(element.getOptionsKey().indexOf('oldOptionValue|'), -1); + }); + }); + + test('saveGroup should save the new groups to tree and return the id of the last group name', () => { + let config = new ConnectionConfig(undefined, undefined, undefined); + let groups: IConnectionProfileGroup[] = configValueToConcat.user; + let expectedLength = configValueToConcat.user.length + 2; + let newGroups: string = 'ROOT/g1/g1-1'; + let color: string = 'red'; + + let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); + assert.notEqual(result, undefined); + assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); + let newGroup = result.groups.find(g => g.name === 'g1-1'); + assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); + }); + + test('saveGroup should only add the groups that are not in the tree', () => { + let config = new ConnectionConfig(undefined, undefined, undefined); + let groups: IConnectionProfileGroup[] = configValueToConcat.user; + let expectedLength = configValueToConcat.user.length + 1; + let newGroups: string = 'ROOT/g2/g2-5'; + let color: string = 'red'; + + let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); + assert.notEqual(result, undefined); + assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); + let newGroup = result.groups.find(g => g.name === 'g2-5'); + assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); + }); + + test('saveGroup should not add any new group if tree already has all the groups in the full path', () => { + let config = new ConnectionConfig(undefined, undefined, undefined); + let groups: IConnectionProfileGroup[] = configValueToConcat.user; + let expectedLength = configValueToConcat.user.length; + let newGroups: string = 'ROOT/g2/g2-1'; + let color: string = 'red'; + + let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); + assert.notEqual(result, undefined); + assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); + let newGroup = result.groups.find(g => g.name === 'g2-1'); + assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); + }); + + test('deleteConnection should remove the connection from config', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length - 1; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = 'database'; + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.deleteConnection(connectionProfile).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('deleteConnectionGroup should remove the children connections and subgroups from config', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = 'database'; + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + let childGroup = new ConnectionProfileGroup('g3-1', connectionProfileGroup, 'g3-1', undefined, undefined); + connectionProfileGroup.addGroups([childGroup]); + connectionProfileGroup.addConnections([connectionProfile]); + + let expectedNumberOfConnections = connections.user.length - 1; + let expectedNumberOfGroups = configValueToConcat.user.length - 2; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.deleteGroup(connectionProfileGroup).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('deleteConnection should not throw error for connection not in config', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'newid', + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.deleteConnection(connectionProfile).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('renameGroup should change group name', done => { + + let expectedNumberOfConnections = configValueToConcat.user.length; + let calledValue: any; + let called: boolean = false; + let nothing: void; + let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { + calledValue = val.value as IConnectionProfileStore[]; + }).returns(() => TPromise.as(nothing)); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfileGroup = new ConnectionProfileGroup('g-renamed', undefined, 'g2', undefined, undefined); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.editGroup(connectionProfileGroup).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + calledValue.forEach(con => { + if (con.id === 'g2') { + assert.equal(con.name, 'g-renamed', 'Group was not renamed'); + called = true; + } + }); + assert.equal(called, true, 'group was not renamed'); + }).then(() => done(), (err) => done(err)); + }); + + + test('change group(parent) for connection group', done => { + let expectedNumberOfConnections = configValueToConcat.user.length; + let calledValue: any; + let nothing: void; + let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { + calledValue = val.value as IConnectionProfileStore[]; + }).returns(() => TPromise.as(nothing)); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let sourceProfileGroup = new ConnectionProfileGroup('g2', undefined, 'g2', undefined, undefined); + let targetProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.changeGroupIdForConnectionGroup(sourceProfileGroup, targetProfileGroup).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + calledValue.forEach(con => { + if (con.id === 'g2') { + assert.equal(con.parentId, 'g3', 'Group parent was not changed'); + } + }); + }).then(() => done(), (err) => done(err)); + }); + + + test('change group(parent) for connection', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + getOptionsKey: () => { return 'connectionId'; }, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: 'test', + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + let newId = 'newid'; + let calledValue: any; + let nothing: void; + let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { + calledValue = val.value as IConnectionProfileStore[]; + }).returns(() => TPromise.as(nothing)); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).callback((x: any, val: any) => { + + }).returns(() => TPromise.as(nothing)); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.changeGroupIdForConnection(connectionProfile, newId).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.atLeastOnce()); + calledValue.forEach(con => { + }); + }).then(() => done(), (err) => done(err)); + }); test('fixConnectionIds should replace duplicate ids with new ones', (done) => { + let profiles: IConnectionProfileStore[] = [ + { + options: {}, + groupId: '1', + id: '1', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '2', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '3', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '2', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '4', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '3', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '2', + providerName: undefined, + savePassword: true, + } + ]; + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.fixConnectionIds(profiles); + let ids = profiles.map(x => x.id); + for (var index = 0; index < ids.length; index++) { + var id = ids[index]; + assert.equal(ids.lastIndexOf(id), index); + } done(); }); + test('addConnection should not move the connection when editing', async () => { + // Set up the connection config + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + + // Clone a connection and modify an option + const connectionIndex = 1; + const optionKey = 'testOption'; + const optionValue = 'testValue'; + let allConnections = config.getConnections(false); + let oldLength = allConnections.length; + let connectionToEdit = allConnections[connectionIndex].clone(); + connectionToEdit.options[optionKey] = optionValue; + await config.addConnection(connectionToEdit); + + // Get the connection and verify that it is in the same place and has been updated + let newConnections = config.getConnections(false); + assert.equal(newConnections.length, oldLength); + let editedConnection = newConnections[connectionIndex]; + assert.equal(editedConnection.getOptionsKey(), connectionToEdit.getOptionsKey()); + assert.equal(editedConnection.options[optionKey], optionValue); + }); + }); From c3a81b5bf3ca30aca9863c8b366fdb4134ae3b68 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 9 Oct 2018 19:43:55 -0700 Subject: [PATCH 25/26] Add back Azure Resource Explorer extension with updated build script (#2805) * Revert "Revert "Port the Azure Resource Explorer extension to core." (#2770)" This reverts commit 210447cd3716d0ad5c12e1a81649de3f9ac5ccf2. * WIP1 * Add custom build task for azurecore extension * Fix azurecore output path for macOS build * Fix linux gulp task name --- build/gulpfile.vscode.js | 52 +- extensions/azurecore/.vscodeignore | 0 extensions/azurecore/README.md | 21 + extensions/azurecore/package.json | 128 ++++ extensions/azurecore/package.nls.json | 13 + extensions/azurecore/resources/azure.svg | 6 + .../resources/dark/account_inverse.svg | 1 + .../resources/dark/connect_to_inverse.svg | 1 + .../resources/dark/filter_inverse.svg | 1 + .../resources/dark/folder_inverse.svg | 1 + .../resources/dark/refresh_inverse.svg | 1 + .../resources/dark/sql_database_inverse.svg | 1 + .../resources/dark/sql_server_inverse.svg | 1 + .../resources/dark/subscription_inverse.svg | 1 + .../azurecore/resources/light/account.svg | 1 + .../azurecore/resources/light/connect_to.svg | 1 + .../azurecore/resources/light/filter.svg | 1 + .../azurecore/resources/light/folder.svg | 1 + .../azurecore/resources/light/refresh.svg | 1 + .../resources/light/sql_database.svg | 1 + .../azurecore/resources/light/sql_server.svg | 1 + .../resources/light/subscription.svg | 1 + extensions/azurecore/src/apiWrapper.ts | 225 +++++++ extensions/azurecore/src/appContext.ts | 28 + .../azurecore/src/azureResource/commands.ts | 120 ++++ .../azurecore/src/azureResource/constants.ts | 16 + .../azurecore/src/azureResource/errors.ts | 15 + .../azurecore/src/azureResource/interfaces.ts | 54 ++ .../azurecore/src/azureResource/models.ts | 25 + .../src/azureResource/servicePool.ts | 35 + .../azureResource/services/accountService.ts | 32 + .../azureResource/services/cacheService.ts | 25 + .../azureResource/services/contextService.ts | 36 ++ .../services/credentialService.ts | 43 ++ .../services/databaseServerService.ts | 39 ++ .../azureResource/services/databaseService.ts | 51 ++ .../services/subscriptionFilterService.ts | 77 +++ .../services/subscriptionService.ts | 33 + .../tree/accountNotSignedInTreeNode.ts | 51 ++ .../src/azureResource/tree/accountTreeNode.ts | 155 +++++ .../src/azureResource/tree/baseTreeNodes.ts | 71 ++ .../tree/databaseContainerTreeNode.ts | 103 +++ .../tree/databaseServerContainerTreeNode.ts | 103 +++ .../tree/databaseServerTreeNode.ts | 57 ++ .../azureResource/tree/databaseTreeNode.ts | 61 ++ .../src/azureResource/tree/messageTreeNode.ts | 60 ++ .../tree/subscriptionTreeNode.ts | 65 ++ .../azureResource/tree/treeChangeHandler.ts | 12 + .../src/azureResource/tree/treeProvider.ts | 101 +++ .../azurecore/src/azureResource/utils.ts | 44 ++ extensions/azurecore/src/constants.ts | 8 + .../src/controllers/controllerBase.ts | 34 + .../src/controllers/mainController.ts | 54 ++ extensions/azurecore/src/extension.ts | 40 ++ .../tree/accountNotSignedInTreeNode.test.ts | 37 ++ .../tree/accountTreeNode.test.ts | 294 +++++++++ .../tree/databaseContainerTreeNode.test.ts | 219 +++++++ .../databaseServerContainerTreeNode.test.ts | 219 +++++++ .../tree/databaseServerTreeNode.test.ts | 62 ++ .../tree/databaseTreeNode.test.ts | 62 ++ .../tree/messageTreeNode.test.ts | 41 ++ .../tree/subscriptionTreeNode.test.ts | 91 +++ .../azureResource/tree/treeProvider.test.ts | 113 ++++ extensions/azurecore/src/test/index.ts | 30 + extensions/azurecore/src/treeNodes.ts | 77 +++ extensions/azurecore/src/typings/ref.d.ts | 9 + extensions/azurecore/tsconfig.json | 22 + extensions/azurecore/yarn.lock | 608 ++++++++++++++++++ scripts/test-integration.bat | 5 +- scripts/test-integration.sh | 4 +- .../common/connectionManagementService.ts | 10 +- 71 files changed, 4001 insertions(+), 11 deletions(-) create mode 100644 extensions/azurecore/.vscodeignore create mode 100644 extensions/azurecore/README.md create mode 100644 extensions/azurecore/package.json create mode 100644 extensions/azurecore/package.nls.json create mode 100644 extensions/azurecore/resources/azure.svg create mode 100644 extensions/azurecore/resources/dark/account_inverse.svg create mode 100644 extensions/azurecore/resources/dark/connect_to_inverse.svg create mode 100644 extensions/azurecore/resources/dark/filter_inverse.svg create mode 100644 extensions/azurecore/resources/dark/folder_inverse.svg create mode 100644 extensions/azurecore/resources/dark/refresh_inverse.svg create mode 100644 extensions/azurecore/resources/dark/sql_database_inverse.svg create mode 100644 extensions/azurecore/resources/dark/sql_server_inverse.svg create mode 100644 extensions/azurecore/resources/dark/subscription_inverse.svg create mode 100644 extensions/azurecore/resources/light/account.svg create mode 100644 extensions/azurecore/resources/light/connect_to.svg create mode 100644 extensions/azurecore/resources/light/filter.svg create mode 100644 extensions/azurecore/resources/light/folder.svg create mode 100644 extensions/azurecore/resources/light/refresh.svg create mode 100644 extensions/azurecore/resources/light/sql_database.svg create mode 100644 extensions/azurecore/resources/light/sql_server.svg create mode 100644 extensions/azurecore/resources/light/subscription.svg create mode 100644 extensions/azurecore/src/apiWrapper.ts create mode 100644 extensions/azurecore/src/appContext.ts create mode 100644 extensions/azurecore/src/azureResource/commands.ts create mode 100644 extensions/azurecore/src/azureResource/constants.ts create mode 100644 extensions/azurecore/src/azureResource/errors.ts create mode 100644 extensions/azurecore/src/azureResource/interfaces.ts create mode 100644 extensions/azurecore/src/azureResource/models.ts create mode 100644 extensions/azurecore/src/azureResource/servicePool.ts create mode 100644 extensions/azurecore/src/azureResource/services/accountService.ts create mode 100644 extensions/azurecore/src/azureResource/services/cacheService.ts create mode 100644 extensions/azurecore/src/azureResource/services/contextService.ts create mode 100644 extensions/azurecore/src/azureResource/services/credentialService.ts create mode 100644 extensions/azurecore/src/azureResource/services/databaseServerService.ts create mode 100644 extensions/azurecore/src/azureResource/services/databaseService.ts create mode 100644 extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts create mode 100644 extensions/azurecore/src/azureResource/services/subscriptionService.ts create mode 100644 extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/accountTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/messageTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts create mode 100644 extensions/azurecore/src/azureResource/tree/treeProvider.ts create mode 100644 extensions/azurecore/src/azureResource/utils.ts create mode 100644 extensions/azurecore/src/constants.ts create mode 100644 extensions/azurecore/src/controllers/controllerBase.ts create mode 100644 extensions/azurecore/src/controllers/mainController.ts create mode 100644 extensions/azurecore/src/extension.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts create mode 100644 extensions/azurecore/src/test/index.ts create mode 100644 extensions/azurecore/src/treeNodes.ts create mode 100644 extensions/azurecore/src/typings/ref.d.ts create mode 100644 extensions/azurecore/tsconfig.json create mode 100644 extensions/azurecore/yarn.lock diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 51422b2a29..39199be6a7 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -76,6 +76,7 @@ const sqlBuiltInExtensions = [ 'import', 'profiler' ]; +var azureExtensions = [ 'azurecore']; const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.main'), @@ -276,6 +277,40 @@ function packageBuiltInExtensions() { }); } +// {{SQL CARBON EDIT}} +function packageAzureCoreTask(platform, arch) { + var destination = path.join(path.dirname(root), 'azuredatastudio') + (platform ? '-' + platform : '') + (arch ? '-' + arch : ''); + if (platform === 'darwin') { + destination = path.join(destination, 'Azure Data Studio.app', 'Contents', 'Resources', 'app', 'extensions', 'azurecore'); + } else { + destination = path.join(destination, 'resources', 'app', 'extensions', 'azurecore'); + } + + platform = platform || process.platform; + + return () => { + const root = path.resolve(path.join(__dirname, '..')); + const localExtensionDescriptions = glob.sync('extensions/*/package.json') + .map(manifestPath => { + const extensionPath = path.dirname(path.join(root, manifestPath)); + const extensionName = path.basename(extensionPath); + return { name: extensionName, path: extensionPath }; + }) + .filter(({ name }) => azureExtensions.indexOf(name) > -1); + + const localExtensions = es.merge(...localExtensionDescriptions.map(extension => { + return ext.fromLocal(extension.path); + })); + + let result = localExtensions + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()) + .pipe(filter(['**', '!LICENSE', '!LICENSES.chromium.html', '!version'])); + + return result.pipe(vfs.dest(destination)); + }; +} + function packageTask(platform, arch, opts) { opts = opts || {}; @@ -307,8 +342,10 @@ function packageTask(platform, arch, opts) { .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) // {{SQL CARBON EDIT}} - .filter(({ name }) => sqlBuiltInExtensions.indexOf(name) === -1); - packageBuiltInExtensions(); + .filter(({ name }) => sqlBuiltInExtensions.indexOf(name) === -1) + .filter(({ name }) => azureExtensions.indexOf(name) === -1); + + packageBuiltInExtensions(); const localExtensions = es.merge(...localExtensionDescriptions.map(extension => { return ext.fromLocal(extension.path) @@ -325,7 +362,6 @@ function packageTask(platform, arch, opts) { .pipe(util.cleanNodeModule('account-provider-azure', ['node_modules/date-utils/doc/**', 'node_modules/adal_node/node_modules/**'], undefined)) .pipe(util.cleanNodeModule('typescript', ['**/**'], undefined)); - const sources = es.merge(src, localExtensions, localExtensionDependencies) .pipe(util.setExecutableBit(['**/*.sh'])) .pipe(filter(['**', '!**/*.js.map'])); @@ -461,6 +497,10 @@ function packageTask(platform, arch, opts) { const buildRoot = path.dirname(root); // {{SQL CARBON EDIT}} +gulp.task('vscode-win32-x64-azurecore', ['optimize-vscode'], packageAzureCoreTask('win32', 'x64')); +gulp.task('vscode-darwin-azurecore', ['optimize-vscode'], packageAzureCoreTask('darwin')); +gulp.task('vscode-linux-x64-azurecore', ['optimize-vscode'], packageAzureCoreTask('linux', 'x64')); + gulp.task('clean-vscode-win32-ia32', util.rimraf(path.join(buildRoot, 'azuredatastudio-win32-ia32'))); gulp.task('clean-vscode-win32-x64', util.rimraf(path.join(buildRoot, 'azuredatastudio-win32-x64'))); gulp.task('clean-vscode-darwin', util.rimraf(path.join(buildRoot, 'azuredatastudio-darwin'))); @@ -469,10 +509,10 @@ gulp.task('clean-vscode-linux-x64', util.rimraf(path.join(buildRoot, 'azuredatas gulp.task('clean-vscode-linux-arm', util.rimraf(path.join(buildRoot, 'azuredatastudio-linux-arm'))); gulp.task('vscode-win32-ia32', ['optimize-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32')); -gulp.task('vscode-win32-x64', ['optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64')); -gulp.task('vscode-darwin', ['optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin')); +gulp.task('vscode-win32-x64', ['vscode-win32-x64-azurecore', 'optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64')); +gulp.task('vscode-darwin', ['vscode-darwin-azurecore', 'optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin')); gulp.task('vscode-linux-ia32', ['optimize-vscode', 'clean-vscode-linux-ia32'], packageTask('linux', 'ia32')); -gulp.task('vscode-linux-x64', ['optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64')); +gulp.task('vscode-linux-x64', ['vscode-linux-x64-azurecore', 'optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64')); gulp.task('vscode-linux-arm', ['optimize-vscode', 'clean-vscode-linux-arm'], packageTask('linux', 'arm')); gulp.task('vscode-win32-ia32-min', ['minify-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32', { minified: true })); diff --git a/extensions/azurecore/.vscodeignore b/extensions/azurecore/.vscodeignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/azurecore/README.md b/extensions/azurecore/README.md new file mode 100644 index 0000000000..003e776621 --- /dev/null +++ b/extensions/azurecore/README.md @@ -0,0 +1,21 @@ +# Azure (Core) extension for Azure Data Studio + +Welcome to the Azure (Core) extension for Azure Data Studio! This extension supports core Azure functionality such as browsing and connecting to Azure data endpoints. In the current release the following features are supported: + +* Log in to Azure and browse your accounts, subscriptions and data resources +* See Azure SQL Databases and Servers in the tree, and open these connections in Object Explorer +* Filter the list of subscriptions for a given account, to make finding specific databases easier + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Privacy Statement + +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt). diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json new file mode 100644 index 0000000000..d490feffbb --- /dev/null +++ b/extensions/azurecore/package.json @@ -0,0 +1,128 @@ +{ + "name": "azurecore", + "displayName": "%azure.displayName%", + "description": "%azure.description", + "version": "0.1.0", + "publisher": "Microsoft", + "preview": true, + "engines": { + "vscode": "^1.25.0", + "sqlops": "*" + }, + "activationEvents": [ + "onView:azureResourceExplorer" + ], + "main": "./out/extension", + "contributes": { + "configuration": [ + { + "type": "object", + "title": "%azure.config.title%", + "properties": { + "azureResource.resourceFilter": { + "type": "array", + "default": null, + "description": "%azure.resourceFilter.description%" + } + } + } + ], + "commands": [ + { + "command": "azureresource.refreshall", + "title": "%azureresource.refreshall%", + "icon": { + "dark": "resources/dark/refresh_inverse.svg", + "light": "resources/light/refresh.svg" + } + }, + { + "command": "azureresource.refresh", + "title": "%azureresource.refresh%", + "icon": { + "dark": "resources/dark/refresh_inverse.svg", + "light": "resources/light/refresh.svg" + } + }, + { + "command": "azureresource.signin", + "title": "%azureresource.signin%" + }, + { + "command": "azureresource.connectsqldb", + "title": "%azureresource.connectsqldb%", + "icon": { + "dark": "resources/dark/connect_to_inverse.svg", + "light": "resources/light/connect_to.svg" + } + }, + { + "command": "azureresource.selectsubscriptions", + "title": "%azureresource.selectsubscriptions%", + "icon": { + "dark": "resources/dark/filter_inverse.svg", + "light": "resources/light/filter.svg" + } + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "azureResource", + "title": "%azure.title%", + "icon": "resources/azure.svg" + } + ] + }, + "views": { + "azureResource": [ + { + "id": "azureResourceExplorer", + "name": "%azure.resourceExplorer.title%" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "azureresource.refreshall", + "when": "view == azureResourceExplorer", + "group": "navigation@1" + } + ], + "view/item/context": [ + { + "command": "azureresource.connectsqldb", + "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", + "group": "1azureresource@1" + }, + { + "command": "azureresource.connectsqldb", + "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", + "group": "inline" + }, + { + "command": "azureresource.selectsubscriptions", + "when": "viewItem == azureResource.itemType.account", + "group": "inline" + }, + { + "command": "azureresource.refresh", + "when": "viewItem != azureResource.itemType.database && viewItem != azureResource.itemType.databaseServer && viewItem != azureResource.itemType.message", + "group": "inline" + } + ] + } + }, + "dependencies": { + "azure-arm-resource": "^7.0.0", + "azure-arm-sql": "^5.0.1", + "vscode-nls": "^4.0.0" + }, + "devDependencies": { + "@types/mocha": "^5.2.5", + "mocha": "^5.2.0", + "should": "^13.2.1", + "typemoq": "^2.1.0" + } +} diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json new file mode 100644 index 0000000000..5b3abedb0b --- /dev/null +++ b/extensions/azurecore/package.nls.json @@ -0,0 +1,13 @@ +{ + "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" +} \ No newline at end of file diff --git a/extensions/azurecore/resources/azure.svg b/extensions/azurecore/resources/azure.svg new file mode 100644 index 0000000000..dd17231563 --- /dev/null +++ b/extensions/azurecore/resources/azure.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/extensions/azurecore/resources/dark/account_inverse.svg b/extensions/azurecore/resources/dark/account_inverse.svg new file mode 100644 index 0000000000..0429cb4941 --- /dev/null +++ b/extensions/azurecore/resources/dark/account_inverse.svg @@ -0,0 +1 @@ +account_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/connect_to_inverse.svg b/extensions/azurecore/resources/dark/connect_to_inverse.svg new file mode 100644 index 0000000000..9d796bdc76 --- /dev/null +++ b/extensions/azurecore/resources/dark/connect_to_inverse.svg @@ -0,0 +1 @@ +connect_to_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/filter_inverse.svg b/extensions/azurecore/resources/dark/filter_inverse.svg new file mode 100644 index 0000000000..60b90abd6d --- /dev/null +++ b/extensions/azurecore/resources/dark/filter_inverse.svg @@ -0,0 +1 @@ +filter_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/folder_inverse.svg b/extensions/azurecore/resources/dark/folder_inverse.svg new file mode 100644 index 0000000000..f94d427cb1 --- /dev/null +++ b/extensions/azurecore/resources/dark/folder_inverse.svg @@ -0,0 +1 @@ +folder_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/refresh_inverse.svg b/extensions/azurecore/resources/dark/refresh_inverse.svg new file mode 100644 index 0000000000..d79fdaa4e8 --- /dev/null +++ b/extensions/azurecore/resources/dark/refresh_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_database_inverse.svg b/extensions/azurecore/resources/dark/sql_database_inverse.svg new file mode 100644 index 0000000000..5eaaf7e5f0 --- /dev/null +++ b/extensions/azurecore/resources/dark/sql_database_inverse.svg @@ -0,0 +1 @@ +sql_database_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_server_inverse.svg b/extensions/azurecore/resources/dark/sql_server_inverse.svg new file mode 100644 index 0000000000..1ce3f48b39 --- /dev/null +++ b/extensions/azurecore/resources/dark/sql_server_inverse.svg @@ -0,0 +1 @@ +sql_server_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/subscription_inverse.svg b/extensions/azurecore/resources/dark/subscription_inverse.svg new file mode 100644 index 0000000000..491fb5e895 --- /dev/null +++ b/extensions/azurecore/resources/dark/subscription_inverse.svg @@ -0,0 +1 @@ +subscription_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/light/account.svg b/extensions/azurecore/resources/light/account.svg new file mode 100644 index 0000000000..91de1865c0 --- /dev/null +++ b/extensions/azurecore/resources/light/account.svg @@ -0,0 +1 @@ +account \ No newline at end of file diff --git a/extensions/azurecore/resources/light/connect_to.svg b/extensions/azurecore/resources/light/connect_to.svg new file mode 100644 index 0000000000..1f420533d6 --- /dev/null +++ b/extensions/azurecore/resources/light/connect_to.svg @@ -0,0 +1 @@ +connect_to \ No newline at end of file diff --git a/extensions/azurecore/resources/light/filter.svg b/extensions/azurecore/resources/light/filter.svg new file mode 100644 index 0000000000..32f914f54a --- /dev/null +++ b/extensions/azurecore/resources/light/filter.svg @@ -0,0 +1 @@ +filter_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/folder.svg b/extensions/azurecore/resources/light/folder.svg new file mode 100644 index 0000000000..517c9b185d --- /dev/null +++ b/extensions/azurecore/resources/light/folder.svg @@ -0,0 +1 @@ +folder_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/refresh.svg b/extensions/azurecore/resources/light/refresh.svg new file mode 100644 index 0000000000..e034574819 --- /dev/null +++ b/extensions/azurecore/resources/light/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_database.svg b/extensions/azurecore/resources/light/sql_database.svg new file mode 100644 index 0000000000..e0c1584d98 --- /dev/null +++ b/extensions/azurecore/resources/light/sql_database.svg @@ -0,0 +1 @@ +sql_database \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_server.svg b/extensions/azurecore/resources/light/sql_server.svg new file mode 100644 index 0000000000..03cff4d932 --- /dev/null +++ b/extensions/azurecore/resources/light/sql_server.svg @@ -0,0 +1 @@ +sql_server \ No newline at end of file diff --git a/extensions/azurecore/resources/light/subscription.svg b/extensions/azurecore/resources/light/subscription.svg new file mode 100644 index 0000000000..5fda717712 --- /dev/null +++ b/extensions/azurecore/resources/light/subscription.svg @@ -0,0 +1 @@ +subscription \ No newline at end of file diff --git a/extensions/azurecore/src/apiWrapper.ts b/extensions/azurecore/src/apiWrapper.ts new file mode 100644 index 0000000000..7a13231e26 --- /dev/null +++ b/extensions/azurecore/src/apiWrapper.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as sqlops from 'sqlops'; + +import * as constants from './constants'; + +/** + * Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into + * this API from our code + * + * @export + * @class ApiWrapper + */ +export class ApiWrapper { + // Data APIs + public registerConnectionProvider(provider: sqlops.ConnectionProvider): vscode.Disposable { + return sqlops.dataprotocol.registerConnectionProvider(provider); + } + + public registerObjectExplorerProvider(provider: sqlops.ObjectExplorerProvider): vscode.Disposable { + return sqlops.dataprotocol.registerObjectExplorerProvider(provider); + } + + public registerTaskServicesProvider(provider: sqlops.TaskServicesProvider): vscode.Disposable { + return sqlops.dataprotocol.registerTaskServicesProvider(provider); + } + + public registerFileBrowserProvider(provider: sqlops.FileBrowserProvider): vscode.Disposable { + return sqlops.dataprotocol.registerFileBrowserProvider(provider); + } + + public registerCapabilitiesServiceProvider(provider: sqlops.CapabilitiesProvider): vscode.Disposable { + return sqlops.dataprotocol.registerCapabilitiesServiceProvider(provider); + } + + public registerModelViewProvider(widgetId: string, handler: (modelView: sqlops.ModelView) => void): void { + return sqlops.ui.registerModelViewProvider(widgetId, handler); + } + + public registerWebviewProvider(widgetId: string, handler: (webview: sqlops.DashboardWebview) => void): void { + return sqlops.dashboard.registerWebviewProvider(widgetId, handler); + } + + public createDialog(title: string): sqlops.window.modelviewdialog.Dialog { + return sqlops.window.modelviewdialog.createDialog(title); + } + + public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + return sqlops.window.modelviewdialog.openDialog(dialog); + } + + public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + return sqlops.window.modelviewdialog.closeDialog(dialog); + } + + public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void { + sqlops.tasks.registerTask(taskId, handler); + } + + public startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void { + sqlops.tasks.startBackgroundOperation(operationInfo); + } + + public getActiveConnections(): Thenable { + return sqlops.connection.getActiveConnections(); + } + + public getCurrentConnection(): Thenable { + return sqlops.connection.getCurrentConnection(); + } + + public createModelViewEditor(title: string, options?: sqlops.ModelViewEditorOptions): sqlops.workspace.ModelViewEditor { + return sqlops.workspace.createModelViewEditor(title, options); + } + + // VSCode APIs + public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { + return vscode.window.createTerminal(name, shellPath, shellArgs); + } + + public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { + return vscode.window.createTerminal(options); + } + + public executeCommand(command: string, ...rest: any[]): Thenable { + return vscode.commands.executeCommand(command, ...rest); + } + + public getFilePathRelativeToWorkspace(uri: vscode.Uri): string { + return vscode.workspace.asRelativePath(uri); + } + + public getWorkspaceFolders(): vscode.WorkspaceFolder[] { + return vscode.workspace.workspaceFolders; + } + + public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined { + let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { + return vscode.commands.registerCommand(command, callback, thisArg); + } + + public registerDocumentOpenHandler(handler: (doc: vscode.TextDocument) => any): vscode.Disposable { + return vscode.workspace.onDidOpenTextDocument(handler); + } + + public registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { + return vscode.window.registerTreeDataProvider(viewId, treeDataProvider); + } + + public setCommandContext(key: string, value: any): Thenable { + return vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, key, value); + } + + /** + * Get the configuration for a extensionName + * @param extensionName The string name of the extension to get the configuration for + * @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations + */ + public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration { + if (typeof resource === 'string') { + try { + resource = this.parseUri(resource); + } catch (e) { + resource = undefined; + } + } + return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri); + } + + public getExtensionConfiguration(): vscode.WorkspaceConfiguration { + return this.getConfiguration(constants.extensionConfigSectionName); + } + + /** + * Parse uri + */ + public parseUri(uri: string): vscode.Uri { + return vscode.Uri.parse(uri); + } + + public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { + return vscode.window.showOpenDialog(options); + } + + public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { + return vscode.window.showSaveDialog(options); + } + + public openTextDocument(uri: vscode.Uri): Thenable; + public openTextDocument(options: { language?: string; content?: string; }): Thenable; + public openTextDocument(uriOrOptions): Thenable { + return vscode.workspace.openTextDocument(uriOrOptions); + } + + public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean, preview?: boolean): Thenable { + let options: vscode.TextDocumentShowOptions = { + viewColumn: column, + preserveFocus: preserveFocus, + preview: preview + }; + return vscode.window.showTextDocument(document, options); + } + + public showErrorMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showErrorMessage(message, ...items); + } + + public showWarningMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showWarningMessage(message, ...items); + } + + public showInformationMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showInformationMessage(message, ...items); + } + + public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { + return vscode.window.createStatusBarItem(alignment, priority); + } + + public get workspaceFolders(): vscode.WorkspaceFolder[] { + return vscode.workspace.workspaceFolders; + } + + public createOutputChannel(name: string): vscode.OutputChannel { + return vscode.window.createOutputChannel(name); + } + + public createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage { + return sqlops.window.modelviewdialog.createWizardPage(title); + } + + public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { + return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters); + } + + public createTab(title: string): sqlops.window.modelviewdialog.DialogTab { + return sqlops.window.modelviewdialog.createTab(title); + } + + // Account APIs + public getAllAccounts(): Thenable { + return sqlops.accounts.getAllAccounts(); + } + + public getSecurityToken(account: sqlops.Account): Thenable<{}> { + return sqlops.accounts.getSecurityToken(account); + } + + public readonly onDidChangeAccounts = sqlops.accounts.onDidChangeAccounts; + + // Connection APIs + public openConnectionDialog(providers: string[], initialConnectionProfile?: sqlops.IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Thenable { + return sqlops.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions); + } +} diff --git a/extensions/azurecore/src/appContext.ts b/extensions/azurecore/src/appContext.ts new file mode 100644 index 0000000000..4abec27439 --- /dev/null +++ b/extensions/azurecore/src/appContext.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import { ApiWrapper } from './apiWrapper'; + +/** + * Global context for the application + */ +export class AppContext { + + private serviceMap: Map = new Map(); + constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) { + this.apiWrapper = apiWrapper || new ApiWrapper(); + } + + public getService(serviceName: string): T { + return this.serviceMap.get(serviceName) as T; + } + + public registerService(serviceName: string, service: T): void { + this.serviceMap.set(serviceName, service); + } +} diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts new file mode 100644 index 0000000000..0edb925c6f --- /dev/null +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { window, QuickPickItem } from 'vscode'; +import { IConnectionProfile } from 'sqlops'; +import { generateGuid } from './utils'; +import { ApiWrapper } from '../apiWrapper'; +import { TreeNode } from '../treeNodes'; + +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'; + +export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: AzureResourceTreeProvider): void { + apiWrapper.registerCommand('azureresource.selectsubscriptions', async (node?: TreeNode) => { + if (!(node instanceof AzureResourceAccountTreeNode)) { + return; + } + + const accountNode = node as AzureResourceAccountTreeNode; + + const servicePool = AzureResourceServicePool.getInstance(); + + let subscriptions = await accountNode.getCachedSubscriptions(); + if (!subscriptions || subscriptions.length === 0) { + const credentials = await servicePool.credentialService.getCredentials(accountNode.account); + subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials); + } + + const selectedSubscriptions = (await servicePool.subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || []; + const selectedSubscriptionIds: string[] = []; + if (selectedSubscriptions.length > 0) { + selectedSubscriptionIds.push(...selectedSubscriptions.map((subscription) => subscription.id)); + } else { + // ALL subscriptions are selected by default + selectedSubscriptionIds.push(...subscriptions.map((subscription) => subscription.id)); + } + + interface SubscriptionQuickPickItem extends QuickPickItem { + subscription: AzureResourceSubscription; + } + + const subscriptionItems: SubscriptionQuickPickItem[] = subscriptions.map((subscription) => { + return { + label: subscription.name, + picked: selectedSubscriptionIds.indexOf(subscription.id) !== -1, + subscription: subscription + }; + }); + + const pickedSubscriptionItems = (await window.showQuickPick(subscriptionItems, { canPickMany: true })); + if (pickedSubscriptionItems && pickedSubscriptionItems.length > 0) { + tree.refresh(node, false); + + const pickedSubscriptions = pickedSubscriptionItems.map((subscriptionItem) => subscriptionItem.subscription); + await servicePool.subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, pickedSubscriptions); + } + }); + + apiWrapper.registerCommand('azureresource.refreshall', () => tree.notifyNodeChanged(undefined)); + + apiWrapper.registerCommand('azureresource.refresh', async (node?: TreeNode) => { + tree.refresh(node, true); + }); + + apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => { + let connectionProfile: 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'); + }); +} diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts new file mode 100644 index 0000000000..3fb5368927 --- /dev/null +++ b/extensions/azurecore/src/azureResource/constants.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +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' +} diff --git a/extensions/azurecore/src/azureResource/errors.ts b/extensions/azurecore/src/azureResource/errors.ts new file mode 100644 index 0000000000..c0fe4a842f --- /dev/null +++ b/extensions/azurecore/src/azureResource/errors.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * 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 class AzureResourceCredentialError extends Error { + constructor( + message: string, + public innerError: Error + ) { + super(message); + } +} diff --git a/extensions/azurecore/src/azureResource/interfaces.ts b/extensions/azurecore/src/azureResource/interfaces.ts new file mode 100644 index 0000000000..5d21c5d408 --- /dev/null +++ b/extensions/azurecore/src/azureResource/interfaces.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account, DidChangeAccountsParams } from 'sqlops'; +import { Event } from 'vscode'; + +import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models'; + +export interface IAzureResourceAccountService { + getAccounts(): Promise; + + readonly onDidChangeAccounts: Event; +} + +export interface IAzureResourceCredentialService { + getCredentials(account: Account): Promise; +} + +export interface IAzureResourceSubscriptionService { + getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceSubscriptionFilterService { + getSelectedSubscriptions(account: Account): Promise; + + saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise; +} + +export interface IAzureResourceDatabaseServerService { + getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceDatabaseService { + getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceCacheService { + get(key: string): T | undefined; + + update(key: string, value: T): void; +} + +export interface IAzureResourceContextService { + getAbsolutePath(relativePath: string): string; + + executeCommand(commandId: string, ...args: any[]): void; + + showErrorMessage(errorMessage: string): void; +} diff --git a/extensions/azurecore/src/azureResource/models.ts b/extensions/azurecore/src/azureResource/models.ts new file mode 100644 index 0000000000..21e5b4ab37 --- /dev/null +++ b/extensions/azurecore/src/azureResource/models.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 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; +} diff --git a/extensions/azurecore/src/azureResource/servicePool.ts b/extensions/azurecore/src/azureResource/servicePool.ts new file mode 100644 index 0000000000..27aff58d98 --- /dev/null +++ b/extensions/azurecore/src/azureResource/servicePool.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { + 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(); +} diff --git a/extensions/azurecore/src/azureResource/services/accountService.ts b/extensions/azurecore/src/azureResource/services/accountService.ts new file mode 100644 index 0000000000..bc38f823a5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/accountService.ts @@ -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 { Event } from 'vscode'; +import { Account, DidChangeAccountsParams } from 'sqlops'; +import { ApiWrapper } from '../../apiWrapper'; + +import { IAzureResourceAccountService } from '../interfaces'; + +export class AzureResourceAccountService implements IAzureResourceAccountService { + public constructor( + apiWrapper: ApiWrapper + ) { + this._apiWrapper = apiWrapper; + this._onDidChangeAccounts = this._apiWrapper.onDidChangeAccounts; + } + + public async getAccounts(): Promise { + return await this._apiWrapper.getAllAccounts(); + } + + public get onDidChangeAccounts(): Event { + return this._onDidChangeAccounts; + } + + private _apiWrapper: ApiWrapper = undefined; + private _onDidChangeAccounts: Event = undefined; +} diff --git a/extensions/azurecore/src/azureResource/services/cacheService.ts b/extensions/azurecore/src/azureResource/services/cacheService.ts new file mode 100644 index 0000000000..ba0d100273 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/cacheService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IAzureResourceCacheService } from "../interfaces"; + +export class AzureResourceCacheService implements IAzureResourceCacheService { + public constructor( + public readonly context: ExtensionContext + ) { + } + + public get(key: string): T | undefined { + return this.context.workspaceState.get(key); + } + + public update(key: string, value: T): void { + this.context.workspaceState.update(key, value); + } +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/services/contextService.ts b/extensions/azurecore/src/azureResource/services/contextService.ts new file mode 100644 index 0000000000..0e153d48e5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/contextService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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; +} diff --git a/extensions/azurecore/src/azureResource/services/credentialService.ts b/extensions/azurecore/src/azureResource/services/credentialService.ts new file mode 100644 index 0000000000..028181fb87 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/credentialService.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } 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: Account): Promise { + try { + let credentials: TokenCredentials[] = []; + let tokens = await this._apiWrapper.getSecurityToken(account); + + 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; +} diff --git a/extensions/azurecore/src/azureResource/services/databaseServerService.ts b/extensions/azurecore/src/azureResource/services/databaseServerService.ts new file mode 100644 index 0000000000..9145629a34 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/databaseServerService.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'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 { + 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; + } +} diff --git a/extensions/azurecore/src/azureResource/services/databaseService.ts b/extensions/azurecore/src/azureResource/services/databaseService.ts new file mode 100644 index 0000000000..643db5c1e6 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/databaseService.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + 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; + } +} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts new file mode 100644 index 0000000000..9e6bea1b08 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { WorkspaceConfiguration, ConfigurationTarget } from 'vscode'; +import { Account } from 'sqlops'; + +import { IAzureResourceSubscriptionFilterService, IAzureResourceCacheService } from '../interfaces'; +import { AzureResourceSubscription } from '../models'; + +interface AzureResourceSelectedSubscriptionsCache { + selectedSubscriptions: { [accountId: string]: AzureResourceSubscription[]}; +} + +export class AzureResourceSubscriptionFilterService implements IAzureResourceSubscriptionFilterService { + public constructor( + cacheService: IAzureResourceCacheService + ) { + this._cacheService = cacheService; + } + + public async getSelectedSubscriptions(account: Account): Promise { + let selectedSubscriptions: AzureResourceSubscription[] = []; + + const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + if (cache) { + selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId]; + } + + return selectedSubscriptions; + } + + public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise { + let selectedSubscriptionsCache: { [accountId: string]: AzureResourceSubscription[]} = {}; + + const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + if (cache) { + selectedSubscriptionsCache = cache.selectedSubscriptions; + } + + if (!selectedSubscriptionsCache) { + selectedSubscriptionsCache = {}; + } + + selectedSubscriptionsCache[account.key.accountId] = selectedSubscriptions; + + this._cacheService.update(AzureResourceSubscriptionFilterService.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(AzureResourceSubscriptionFilterService.FilterConfigName); + let configTarget = ConfigurationTarget.Global; + if (resourceFilterConfig) { + if (resourceFilterConfig.workspaceFolderValue) { + configTarget = ConfigurationTarget.WorkspaceFolder; + } else if (resourceFilterConfig.workspaceValue) { + configTarget = ConfigurationTarget.Workspace; + } else if (resourceFilterConfig.globalValue) { + configTarget = ConfigurationTarget.Global; + } + } + + await this._config.update(AzureResourceSubscriptionFilterService.FilterConfigName, filters, configTarget); + } + + private _config: WorkspaceConfiguration = undefined; + private _cacheService: IAzureResourceCacheService = undefined; + + private static readonly FilterConfigName = 'resourceFilter'; + private static readonly CacheKey = 'azureResource.cache.selectedSubscriptions'; +} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionService.ts b/extensions/azurecore/src/azureResource/services/subscriptionService.ts new file mode 100644 index 0000000000..9a8cfe0779 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/subscriptionService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } from 'sqlops'; +import { ServiceClientCredentials } from 'ms-rest'; +import { SubscriptionClient } from 'azure-arm-resource'; + +import { IAzureResourceSubscriptionService } from '../interfaces'; +import { AzureResourceSubscription } from '../models'; + +export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService { + public async getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise { + 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. + } + } + + return subscriptions; + } +} diff --git a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts new file mode 100644 index 0000000000..47008428aa --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceItemType } from '../constants'; + +export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { + public getChildren(): TreeNode[] | Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.SignInLabel, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.message; + item.command = { + title: AzureResourceAccountNotSignedInTreeNode.SignInLabel, + command: 'azureresource.signin', + arguments: [this] + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: AzureResourceAccountNotSignedInTreeNode.SignInLabel, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.message, + nodeSubType: undefined, + iconType: AzureResourceItemType.message + }; + } + + public get nodePathValue(): string { + return 'message_accountNotSignedIn'; + } + + private static readonly SignInLabel = localize('azureResource.tree.accountNotSignedInTreeNode.signIn', 'Sign in to Azure ...'); +} diff --git a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts new file mode 100644 index 0000000000..a95a485ece --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceErrorMessageUtil } from '../utils'; +import { AzureResourceSubscription } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeProvider'; + +export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase { + public constructor( + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler + ) { + super(account, treeChangeHandler, undefined); + + this._id = `account_${this.account.key.accountId}`; + this._label = this.generateLabel(); + } + + public async getChildren(): Promise { + try { + let subscriptions: AzureResourceSubscription[] = []; + + if (this._isClearingCache) { + const credentials = await this.getCredentials(); + subscriptions = (await this.servicePool.subscriptionService.getSubscriptions(this.account, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { subscriptions: { } }; + } + cache.subscriptions[this.account.key.accountId] = subscriptions; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + subscriptions = await this.getCachedSubscriptions(); + } + + this._totalSubscriptionCount = subscriptions.length; + + let selectedSubscriptions = await this.servicePool.subscriptionFilterService.getSelectedSubscriptions(this.account); + let selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id); + if (selectedSubscriptionIds.length > 0) { + subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1); + this._selectedSubscriptionCount = selectedSubscriptionIds.length; + } else { + // ALL subscriptions are listed by default + this._selectedSubscriptionCount = this._totalSubscriptionCount; + } + + this.refreshLabel(); + + if (subscriptions.length === 0) { + return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.NoSubscriptions, this)]; + } else { + return subscriptions.map((subscription) => new AzureResourceSubscriptionTreeNode(subscription, this.account, this.treeChangeHandler, this)); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } + } + + public async getCachedSubscriptions(): Promise { + const subscriptions: AzureResourceSubscription[] = []; + const cache = this.getCache(); + if (cache) { + subscriptions.push(...cache.subscriptions[this.account.key.accountId]); + } + return subscriptions; + } + + public getTreeItem(): TreeItem | Promise { + let 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') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this._label, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.account, + nodeSubType: undefined, + iconType: AzureResourceItemType.account + }; + } + + public get nodePathValue(): string { + return this._id; + } + + public get totalSubscriptionCount(): number { + return this._totalSubscriptionCount; + } + + public get selectedSubscriptionCount(): number { + return this._selectedSubscriptionCount; + } + + protected refreshLabel(): void { + const newLabel = this.generateLabel(); + if (this._label !== newLabel) { + this._label = newLabel; + this.treeChangeHandler.notifyNodeChanged(this); + } + } + + protected get cacheKey(): string { + return 'azureResource.cache.subscriptions'; + } + + private generateLabel(): string { + let label = `${this.account.displayInfo.displayName} (${this.account.key.accountId})`; + + if (this._totalSubscriptionCount !== 0) { + label += ` (${this._selectedSubscriptionCount} / ${this._totalSubscriptionCount} subscriptions)`; + } + + return label; + } + + 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[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts new file mode 100644 index 0000000000..ecea876ec1 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } from 'sqlops'; +import { ServiceClientCredentials } from 'ms-rest'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceServicePool } from '../servicePool'; +import { AzureResourceCredentialError } from '../errors'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; + +export abstract class AzureResourceTreeNodeBase extends TreeNode { + public constructor( + public readonly treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(); + + this.parent = parent; + } + + public readonly servicePool = AzureResourceServicePool.getInstance(); +} + +export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase { + public constructor( + public readonly account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + } + + public clearCache(): void { + this._isClearingCache = true; + } + + public get isClearingCache(): boolean { + return this._isClearingCache; + } + + protected async getCredentials(): Promise { + try { + return await this.servicePool.credentialService.getCredentials(this.account); + } catch (error) { + if (error instanceof AzureResourceCredentialError) { + this.servicePool.contextService.showErrorMessage(error.message); + + this.servicePool.contextService.executeCommand('azureresource.signin'); + } else { + throw error; + } + } + } + + protected updateCache(cache: T): void { + this.servicePool.cacheService.update(this.cacheKey, cache); + } + + protected getCache(): T { + return this.servicePool.cacheService.get(this.cacheKey); + } + + protected abstract get cacheKey(): string; + + protected _isClearingCache = true; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts new file mode 100644 index 0000000000..26d5f6d70c --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + try { + let databases: AzureResourceDatabase[] = []; + + if (this._isClearingCache) { + let credentials = await this.getCredentials(); + databases = (await this.servicePool.databaseService.getDatabases(this.subscription, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { databases: { } }; + } + cache.databases[this.subscription.id] = databases; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + const cache = this.getCache(); + if (cache) { + databases = cache.databases[this.subscription.id] || []; + } + } + + 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 { + 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[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts new file mode 100644 index 0000000000..6f2921e092 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + try { + let databaseServers: AzureResourceDatabaseServer[] = []; + + if (this._isClearingCache) { + let credentials = await this.getCredentials(); + databaseServers = (await this.servicePool.databaseServerService.getDatabaseServers(this.subscription, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { databaseServers: { } }; + } + cache.databaseServers[this.subscription.id] = databaseServers; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + const cache = this.getCache(); + if (cache) { + databaseServers = cache.databaseServers[this.subscription.id] || []; + } + } + + 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 { + 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[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts new file mode 100644 index 0000000000..2c8cf5bd4b --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'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 { + return []; + } + + public getTreeItem(): TreeItem | Promise { + 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}`; + } +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts new file mode 100644 index 0000000000..1089a6e0c5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { 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 { + return []; + } + + public getTreeItem(): TreeItem | Promise { + 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; +} diff --git a/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts b/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts new file mode 100644 index 0000000000..edea2e5a76 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceItemType } from '../constants'; + +export class AzureResourceMessageTreeNode extends TreeNode { + public constructor( + public readonly message: string, + parent: TreeNode + ) { + super(); + + this.parent = parent; + this._id = `message_${AzureResourceMessageTreeNode._messageNum++}`; + } + + public static create(message: string, parent: TreeNode): AzureResourceMessageTreeNode { + return new AzureResourceMessageTreeNode(message, parent); + } + + public getChildren(): TreeNode[] | Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(this.message, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.message; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this.message, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.message, + nodeSubType: undefined, + iconType: AzureResourceItemType.message + }; + } + + public get nodePathValue(): string { + return this._id; + } + + private _id: string; + + private static _messageNum: number = 0; +} diff --git a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts new file mode 100644 index 0000000000..ff8474ca3f --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Account, NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceTreeNodeBase, AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceDatabaseContainerTreeNode } from './databaseContainerTreeNode'; +import { AzureResourceDatabaseServerContainerTreeNode } from './databaseServerContainerTreeNode'; +import { AzureResourceSubscription } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; + +export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase { + public constructor( + public readonly subscription: AzureResourceSubscription, + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + + this._children.push(new AzureResourceDatabaseContainerTreeNode(subscription, account, treeChangeHandler, this)); + this._children.push(new AzureResourceDatabaseServerContainerTreeNode(subscription, account, treeChangeHandler, this)); + } + + public async getChildren(): Promise { + return this._children; + } + + public getTreeItem(): TreeItem | Promise { + let 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') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this.subscription.name, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.subscription, + nodeSubType: undefined, + iconType: AzureResourceItemType.subscription + }; + } + + public get nodePathValue(): string { + return `subscription_${this.subscription.id}`; + } + + private _children: AzureResourceContainerTreeNodeBase[] = []; +} diff --git a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts new file mode 100644 index 0000000000..b7db2ed6b4 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeNode } from '../../treeNodes'; + +export interface IAzureResourceTreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts new file mode 100644 index 0000000000..7cfd4ce89f --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/treeProvider.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeDataProvider, EventEmitter, Event, TreeItem } from 'vscode'; +import { DidChangeAccountsParams } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import { setInterval, clearInterval } from 'timers'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceServicePool } from '../servicePool'; +import { AzureResourceAccountTreeNode } from './accountTreeNode'; +import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceContainerTreeNodeBase, AzureResourceTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceErrorMessageUtil } from '../utils'; + +export interface IAzureResourceTreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} + +export class AzureResourceTreeProvider implements TreeDataProvider, IAzureResourceTreeChangeHandler { + public constructor() { + AzureResourceServicePool.getInstance().accountService.onDidChangeAccounts((e: DidChangeAccountsParams) => { this._onDidChangeTreeData.fire(undefined); }); + } + + public async getChildren(element?: TreeNode): Promise { + if (element) { + return element.getChildren(true); + } + + if (!this.isSystemInitialized) { + this._loadingTimer = setInterval(async () => { + try { + // Call sqlops.accounts.getAllAccounts() to determine whether the system has been initialized. + await AzureResourceServicePool.getInstance().accountService.getAccounts(); + + // System has been initialized + this.isSystemInitialized = true; + + if (this._loadingTimer) { + clearInterval(this._loadingTimer); + } + + this._onDidChangeTreeData.fire(undefined); + } catch (error) { + // System not initialized yet + this.isSystemInitialized = false; + } + }, AzureResourceTreeProvider.LoadingTimerInterval); + + return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.Loading, undefined)]; + } + + try { + const accounts = await AzureResourceServicePool.getInstance().accountService.getAccounts(); + + if (accounts && accounts.length > 0) { + return accounts.map((account) => new AzureResourceAccountTreeNode(account, this)); + } else { + return [new AzureResourceAccountNotSignedInTreeNode()]; + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), undefined)]; + } + } + + public get onDidChangeTreeData(): Event { + return this._onDidChangeTreeData.event; + } + + public notifyNodeChanged(node: TreeNode): void { + this._onDidChangeTreeData.fire(node); + } + + public async refresh(node: TreeNode, isClearingCache: boolean): Promise { + if (isClearingCache) { + if ((node instanceof AzureResourceContainerTreeNodeBase)) { + node.clearCache(); + } + } + + this._onDidChangeTreeData.fire(node); + } + + public getTreeItem(element: TreeNode): TreeItem | Thenable { + return element.getTreeItem(); + } + + public isSystemInitialized: boolean = false; + + private _loadingTimer: NodeJS.Timer = undefined; + private _onDidChangeTreeData = new EventEmitter(); + + private static readonly Loading = localize('azureResource.tree.treeProvider.loading', 'Loading ...'); + private static readonly LoadingTimerInterval = 5000; +} diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts new file mode 100644 index 0000000000..99f177eafb --- /dev/null +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +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)); + } +} + +export function generateGuid(): string { + let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) + let oct: string = ''; + let tmp: number; + /* tslint:disable:no-bitwise */ + for (let a: number = 0; a < 4; a++) { + tmp = (4294967296 * Math.random()) | 0; + oct += hexValues[tmp & 0xF] + + hexValues[tmp >> 4 & 0xF] + + hexValues[tmp >> 8 & 0xF] + + hexValues[tmp >> 12 & 0xF] + + hexValues[tmp >> 16 & 0xF] + + hexValues[tmp >> 20 & 0xF] + + hexValues[tmp >> 24 & 0xF] + + hexValues[tmp >> 28 & 0xF]; + } + + // 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively' + let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0]; + return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12); + /* tslint:enable:no-bitwise */ +} \ No newline at end of file diff --git a/extensions/azurecore/src/constants.ts b/extensions/azurecore/src/constants.ts new file mode 100644 index 0000000000..994cfc1f6a --- /dev/null +++ b/extensions/azurecore/src/constants.ts @@ -0,0 +1,8 @@ +'use strict'; + +export const extensionConfigSectionName = 'azure'; +export const ViewType = 'view'; + +export enum BuiltInCommands { + SetContext = 'setContext' +} diff --git a/extensions/azurecore/src/controllers/controllerBase.ts b/extensions/azurecore/src/controllers/controllerBase.ts new file mode 100644 index 0000000000..15e31972b4 --- /dev/null +++ b/extensions/azurecore/src/controllers/controllerBase.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; + +import { AppContext } from '../appContext'; +import { ApiWrapper } from '../apiWrapper'; + +export default abstract class ControllerBase implements vscode.Disposable { + + public constructor(protected appContext: AppContext) { + } + + protected get apiWrapper(): ApiWrapper { + return this.appContext.apiWrapper; + } + + public get extensionContext(): vscode.ExtensionContext { + return this.appContext && this.appContext.extensionContext; + } + + abstract activate(): Promise; + + abstract deactivate(): void; + + public dispose(): void { + this.deactivate(); + } +} + diff --git a/extensions/azurecore/src/controllers/mainController.ts b/extensions/azurecore/src/controllers/mainController.ts new file mode 100644 index 0000000000..7f38fd8667 --- /dev/null +++ b/extensions/azurecore/src/controllers/mainController.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + 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); + } +} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts new file mode 100644 index 0000000000..9d693b3fec --- /dev/null +++ b/extensions/azurecore/src/extension.ts @@ -0,0 +1,40 @@ +'use strict'; + +import * as vscode from 'vscode'; + +import MainController from './controllers/mainController'; +import { AppContext } from './appContext'; +import ControllerBase from './controllers/controllerBase'; +import { ApiWrapper } from './apiWrapper'; + +let controllers: ControllerBase[] = []; + +// 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()); + let activations: Promise[] = []; + + // Start the main controller + let mainController = new MainController(appContext); + controllers.push(mainController); + extensionContext.subscriptions.push(mainController); + activations.push(mainController.activate()); + + return Promise.all(activations) + .then((results: boolean[]) => { + for (let result of results) { + if (!result) { + return false; + } + } + return true; + }); +} + +// this method is called when your extension is deactivated +export function deactivate() { + for (let controller of controllers) { + controller.deactivate(); + } +} diff --git a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts new file mode 100644 index 0000000000..9a3cb3f866 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts @@ -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 * as should from 'should'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceAccountNotSignedInTreeNode } from '../../../azureResource/tree/accountNotSignedInTreeNode'; + +describe('AzureResourceAccountNotSignedInTreeNode.info', function(): void { + it('Should be correct.', async function(): Promise { + const label = 'Sign in to Azure ...'; + + const treeNode = new AzureResourceAccountNotSignedInTreeNode(); + + should(treeNode.nodePathValue).equal('message_accountNotSignedIn'); + + const treeItem = await treeNode.getTreeItem(); + should(treeItem.label).equal(label); + should(treeItem.contextValue).equal(AzureResourceItemType.message); + 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'); + + const nodeInfo = treeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(label); + should(nodeInfo.nodeType).equal(AzureResourceItemType.message); + should(nodeInfo.iconType).equal(AzureResourceItemType.message); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts new file mode 100644 index 0000000000..27b64e0160 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * 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, + IAzureResourceSubscriptionService, + IAzureResourceSubscriptionFilterService +} 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'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockCacheService: TypeMoq.IMock; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockSubscriptionService: TypeMoq.IMock; +let mockSubscriptionFilterService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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().object; +const mockCredentials = [mockCredential]; + +const mockSubscription1: AzureResourceSubscription = { + id: 'mock_subscription_1', + name: 'mock subscription 1' +}; +const mockSubscription2: AzureResourceSubscription = { + id: 'mock_subscription_2', + name: 'mock subscription 2' +}; +const mockSubscriptions = [mockSubscription1, mockSubscription2]; +const mockFilteredSubscriptions = [mockSubscription1]; + +let mockSubscriptionCache: { subscriptions: { [accountId: string]: AzureResourceSubscription[]} }; + +describe('AzureResourceAccountTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockSubscriptionCache = { subscriptions: {} }; + + mockServicePool.contextService = mockContextService.object; + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.subscriptionService = mockSubscriptionService.object; + mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + }); + + it('Should be correct when created.', async function(): Promise { + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const accountTreeNodeId = `account_${mockAccount.key.accountId}`; + const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId})`; + + should(accountTreeNode.nodePathValue).equal(accountTreeNodeId); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.id).equal(accountTreeNodeId); + should(treeItem.label).equal(accountTreeNodeLabel); + should(treeItem.contextValue).equal(AzureResourceItemType.account); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + should(nodeInfo.isLeaf).false(); + should(nodeInfo.nodeType).equal(AzureResourceItemType.account); + should(nodeInfo.iconType).equal(AzureResourceItemType.account); + }); + + it('Should be correct when there are subscriptions listed.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).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); + + await accountTreeNode.getChildren(); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.label).equal(accountTreeNodeLabel); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + }); + + it('Should be correct when there are subscriptions filtered.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).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); + + await accountTreeNode.getChildren(); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.label).equal(accountTreeNodeLabel); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + }); +}); + +describe('AzureResourceAccountTreeNode.getChildren', function(): void { + beforeEach(() => { + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockSubscriptionCache = { subscriptions: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.subscriptionService = mockSubscriptionService.object; + mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + }); + + it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); + + mockTreeChangeHandler.verify((o) => o.notifyNodeChanged(accountTreeNode), TypeMoq.Times.once()); + + should(accountTreeNode.totalSubscriptionCount).equal(mockSubscriptions.length); + should(accountTreeNode.selectedSubscriptionCount).equal(mockSubscriptions.length); + should(accountTreeNode.isClearingCache).false(); + + 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); + + 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}`); + } + }); + + it('Should load subscriptions from cache when it is not clearing cache.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + await accountTreeNode.getChildren(); + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), 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)); + + should(children.length).equal(mockSubscriptionCache.subscriptions[mockAccount.key.accountId].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}`); + } + }); + + it('Should handle when there is no subscriptions.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + should(accountTreeNode.totalSubscriptionCount).equal(0); + + 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 Subscriptions found.'); + }); + + it('Should honor subscription filtering.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); + + should(accountTreeNode.selectedSubscriptionCount).equal(mockFilteredSubscriptions.length); + should(children.length).equal(mockFilteredSubscriptions.length); + + for (let ix = 0; ix < mockFilteredSubscriptions.length; ix++) { + should(children[ix].nodePathValue).equal(`subscription_${mockFilteredSubscriptions[ix].id}`); + } + }); + + it('Should handle errors.', async function(): Promise { + const mockError = 'Test error'; + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => { throw new Error(mockError); }); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), 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('AzureResourceAccountTreeNode.clearCache', function() : void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should clear cache.', async function(): Promise { + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + accountTreeNode.clearCache(); + should(accountTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts new file mode 100644 index 0000000000..a2e8640265 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * 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; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockDatabaseService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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().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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + 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(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockDatabaseService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockDatabaseContainerCache = { databases: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.databaseService = mockDatabaseService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).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 { + 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), 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 { + 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), 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 { + 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 { + 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), 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(); + }); + + it('Should clear cache.', async function(): Promise { + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + databaseContainerTreeNode.clearCache(); + should(databaseContainerTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts new file mode 100644 index 0000000000..5bbed2f812 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * 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; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockDatabaseServerService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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().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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + 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(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockDatabaseServerService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockDatabaseServerContainerCache = { databaseServers: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.databaseServerService = mockDatabaseServerService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).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 { + 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), 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 { + 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), 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 { + 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 { + 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), 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(); + }); + + it('Should clear cache.', async function(): Promise { + const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + databaseServerContainerTreeNode.clearCache(); + should(databaseServerContainerTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts new file mode 100644 index 0000000000..cef163b670 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + 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); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts new file mode 100644 index 0000000000..a1350771b8 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct.', async function(): Promise { + 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); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts new file mode 100644 index 0000000000..ddd867c60f --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts @@ -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 * as should from 'should'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; + +describe('AzureResourceMessageTreeNode.info', function(): void { + it('Should be correct when created.', async function(): Promise { + const mockMessage = 'Test messagse'; + const treeNode = new AzureResourceMessageTreeNode(mockMessage, undefined); + + should(treeNode.nodePathValue).startWith('message_'); + + const treeItem = await treeNode.getTreeItem(); + should(treeItem.label).equal(mockMessage); + should(treeItem.contextValue).equal(AzureResourceItemType.message); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + + const nodeInfo = treeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(mockMessage); + should(nodeInfo.nodeType).equal(AzureResourceItemType.message); + should(nodeInfo.iconType).equal(AzureResourceItemType.message); + }); +}); + +describe('AzureResourceMessageTreeNode.create', function(): void { + it('Should create a message node.', async function(): Promise { + const mockMessage = 'Test messagse'; + const treeNode = AzureResourceMessageTreeNode.create(mockMessage, undefined); + should(treeNode).instanceof(AzureResourceMessageTreeNode); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts new file mode 100644 index 0000000000..e55621dfed --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { IAzureResourceContextService } from '../../../azureResource/interfaces'; +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'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockContextService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// 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: AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +describe('AzureResourceSubscriptionTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + should(subscriptionTreeNode.nodePathValue).equal(`subscription_${mockSubscription.id}`); + + const treeItem = await subscriptionTreeNode.getTreeItem(); + should(treeItem.label).equal(mockSubscription.name); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + should(treeItem.contextValue).equal(AzureResourceItemType.subscription); + + const nodeInfo = subscriptionTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(mockSubscription.name); + should(nodeInfo.isLeaf).equal(false); + should(nodeInfo.nodeType).equal(AzureResourceItemType.subscription); + should(nodeInfo.iconType).equal(AzureResourceItemType.subscription); + }); +}); + +describe('AzureResourceSubscriptionTreeNode.getChildren', function(): void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should load database containers.', async function(): Promise { + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + const children = await subscriptionTreeNode.getChildren(); + + should(children).Array(); + should(children.length).equal(2); + should(children[0]).instanceof(AzureResourceDatabaseContainerTreeNode); + should(children[1]).instanceof(AzureResourceDatabaseServerContainerTreeNode); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts new file mode 100644 index 0000000000..f8cf33c84c --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { 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'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockAccountService: TypeMoq.IMock; + +// Mock test data +const mockAccount1: sqlops.Account = { + key: { + accountId: 'mock_account_1', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account_1@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; +const mockAccount2: sqlops.Account = { + key: { + accountId: 'mock_account_2', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account_2@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; +const mockAccounts = [mockAccount1, mockAccount2]; + +describe('AzureResourceTreeProvider.getChildren', function(): void { + beforeEach(() => { + mockAccountService = TypeMoq.Mock.ofType(); + + mockServicePool.accountService = mockAccountService.object; + }); + + it('Should load accounts.', async function(): Promise { + mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(mockAccounts)); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); + + should(children).Array(); + should(children.length).equal(mockAccounts.length); + + for (let ix = 0; ix < mockAccounts.length; ix++) { + const child = children[ix]; + const account = mockAccounts[ix]; + + should(child).instanceof(AzureResourceAccountTreeNode); + should(child.nodePathValue).equal(`account_${account.key.accountId}`); + } + }); + + it('Should handle when there is no accounts.', async function(): Promise { + mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(undefined)); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceAccountNotSignedInTreeNode); + }); + + it('Should handle errors.', async function(): Promise { + const mockAccountError = 'Test account error'; + mockAccountService.setup((o) => o.getAccounts()).returns(() => { throw new Error(mockAccountError); }); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); + + 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: ${mockAccountError}`); + }); +}); diff --git a/extensions/azurecore/src/test/index.ts b/extensions/azurecore/src/test/index.ts new file mode 100644 index 0000000000..a9f0ffbd82 --- /dev/null +++ b/extensions/azurecore/src/test/index.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('vscode/lib/testrunner'); + +const suite = 'Integration Azure Tests'; + +const options: any = { + ui: 'bdd', + useColors: true, + timeout: 60000 +}; + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/azurecore/src/treeNodes.ts b/extensions/azurecore/src/treeNodes.ts new file mode 100644 index 0000000000..7136ba98bd --- /dev/null +++ b/extensions/azurecore/src/treeNodes.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as sqlops from 'sqlops'; +import * 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) { + path = this.parent.generateNodePath(); + } + path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue; + return path; + } + + public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise { + let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path; + let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath); + return TreeNode.findNode(this, condition, filter, true); + } + + public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise { + if (!node) { + return undefined; + } + + if (condition(node)) { + return node; + } + + let nodeInfo = node.getNodeInfo(); + if (nodeInfo.isLeaf) { + return undefined; + } + + // TODO support filtering by already expanded / not yet expanded + let children = await node.getChildren(false); + if (children) { + for (let child of children) { + if (filter && filter(child)) { + let childNode = await this.findNode(child, condition, filter, expandIfNeeded); + if (childNode) { + return childNode; + } + } + } + } + return undefined; + } + + /** + * The value to use for this node in the node path + */ + public abstract get nodePathValue(): string; + + abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise; + abstract getTreeItem(): vscode.TreeItem | Promise; + + abstract getNodeInfo(): sqlops.NodeInfo; +} diff --git a/extensions/azurecore/src/typings/ref.d.ts b/extensions/azurecore/src/typings/ref.d.ts new file mode 100644 index 0000000000..41e273db7f --- /dev/null +++ b/extensions/azurecore/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// \ No newline at end of file diff --git a/extensions/azurecore/tsconfig.json b/extensions/azurecore/tsconfig.json new file mode 100644 index 0000000000..b341a65dab --- /dev/null +++ b/extensions/azurecore/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", "es2015.promise" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock new file mode 100644 index 0000000000..8c2535a991 --- /dev/null +++ b/extensions/azurecore/yarn.lock @@ -0,0 +1,608 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/mocha@^5.2.5": + version "5.2.5" + resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" + +"@types/node@^8.0.47": + version "8.10.30" + resolved "https://registry.npmjs.org/@types/node/-/node-8.10.30.tgz#2c82cbed5f79d72280c131d2acffa88fbd8dd353" + +adal-node@^0.1.28: + version "0.1.28" + resolved "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485" + dependencies: + "@types/node" "^8.0.47" + async ">=0.6.0" + date-utils "*" + jws "3.x.x" + request ">= 2.52.0" + underscore ">= 1.3.1" + uuid "^3.1.0" + xmldom ">= 0.1.x" + xpath.js "~1.1.0" + +ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async@2.6.0: + version "2.6.0" + resolved "https://registry.npmjs.org/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + dependencies: + lodash "^4.14.0" + +async@>=0.6.0: + version "2.6.1" + resolved "https://registry.npmjs.org/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + dependencies: + lodash "^4.17.10" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + +azure-arm-resource@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/azure-arm-resource/-/azure-arm-resource-7.0.0.tgz#e76fe2195abe354b607346c2fa0f690544176294" + dependencies: + ms-rest "^2.3.3" + ms-rest-azure "^2.5.5" + +azure-arm-sql@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/azure-arm-sql/-/azure-arm-sql-5.0.1.tgz#75c0b115525d2270ab16122d47d0c666aca175d4" + dependencies: + ms-rest "^2.3.3" + ms-rest-azure "^2.5.5" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + dependencies: + tweetnacl "^0.14.3" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +combined-stream@1.0.6: + version "1.0.6" + resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + dependencies: + delayed-stream "~1.0.0" + +commander@2.15.1: + version "2.15.1" + resolved "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-utils@*: + version "1.2.21" + resolved "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +duplexer@^0.1.1: + version "0.1.1" + resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-buffer@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@3.x.x: + version "3.1.5" + resolved "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + +lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4: + version "4.17.11" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + +mime-db@~1.36.0: + version "1.36.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.20" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + dependencies: + mime-db "~1.36.0" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@0.5.1: + version "0.5.1" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +moment@^2.21.0, moment@^2.22.2: + version "2.22.2" + resolved "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + +ms-rest-azure@^2.5.5: + version "2.5.9" + resolved "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.5.9.tgz#8599943e349c91eb367d2d1dcb885017518dc712" + dependencies: + adal-node "^0.1.28" + async "2.6.0" + moment "^2.22.2" + ms-rest "^2.3.2" + request "^2.88.0" + uuid "^3.2.1" + +ms-rest@^2.3.2, ms-rest@^2.3.3: + version "2.3.7" + resolved "https://registry.npmjs.org/ms-rest/-/ms-rest-2.3.7.tgz#8bfc82fb91807643fcaa487c5fc9698cd18a018c" + dependencies: + duplexer "^0.1.1" + is-buffer "^1.1.6" + is-stream "^1.1.0" + moment "^2.21.0" + request "^2.88.0" + through "^2.3.8" + tunnel "0.0.5" + uuid "^3.2.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +postinstall-build@^5.0.1: + version "5.0.3" + resolved "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" + +psl@^1.1.24: + version "1.1.29" + resolved "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +"request@>= 2.52.0", request@^2.88.0: + version "2.88.0" + resolved "https://registry.npmjs.org/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +should-equal@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" + dependencies: + should-type "^1.4.0" + +should-format@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" + dependencies: + should-type "^1.3.0" + should-type-adaptors "^1.0.1" + +should-type-adaptors@^1.0.1: + version "1.1.0" + resolved "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" + dependencies: + should-type "^1.3.0" + should-util "^1.0.0" + +should-type@^1.3.0, should-type@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" + +should-util@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" + +should@^13.2.1: + version "13.2.3" + resolved "https://registry.npmjs.org/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" + dependencies: + should-equal "^2.0.0" + should-format "^3.0.3" + should-type "^1.4.0" + should-type-adaptors "^1.0.1" + should-util "^1.0.0" + +sshpk@^1.7.0: + version "1.14.2" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + safer-buffer "^2.0.2" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + +through@^2.3.8: + version "2.3.8" + resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tunnel@0.0.5: + version "0.0.5" + resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz#d1532254749ed36620fcd1010865495a1fa9d0ae" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typemoq@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" + dependencies: + circular-json "^0.3.1" + lodash "^4.17.4" + postinstall-build "^5.0.1" + +"underscore@>= 1.3.1": + version "1.9.1" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + +uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +"xmldom@>= 0.1.x": + version "0.1.27" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xpath.js@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 929b92b9bc..4e613f5592 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -6,8 +6,11 @@ pushd %~dp0\.. set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% :: Tests in the extension host -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% +:: TODO port over an re-enable API tests +:: 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% 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% . diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index a48257ad4f..fba91607f9 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -13,9 +13,11 @@ fi cd $ROOT # Tests in the extension host -./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 +# TODO port over an re-enable API tests +# ./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 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 . diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index af123e2d0c..80f466fa9e 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -1351,8 +1351,12 @@ export class ConnectionManagementService extends Disposable implements IConnecti * TODO this could be a map reduce operation */ public buildConnectionInfo(connectionString: string, provider: string): Thenable { - return this._providers.get(provider).onReady.then(e => { - return e.buildConnectionInfo(connectionString); - }); + let connectionProvider = this._providers.get(provider); + if (connectionProvider) { + return connectionProvider.onReady.then(e => { + return e.buildConnectionInfo(connectionString); + }); + } + return Promise.resolve(undefined); } } From 5a62035ed7ab1d3712ba39f14b669f06d3faf65d Mon Sep 17 00:00:00 2001 From: ranasaria <41588310+ranasaria@users.noreply.github.com> Date: Wed, 10 Oct 2018 11:24:13 -0700 Subject: [PATCH 26/26] Support to configure logging levels for sqltools services (#2731) * Adding support for configuring SqlTools log levels from user configuration. This also adds changes to see the tail of the sqltoolsservicelayer log file in the newly created 'Output->Log (SqlTools)' channel * Three new user settings control how logging happens. tracingLevel, logRetentionMinutes & logFilesRemovalLimit. Default tracingLevel is set to 'Critical'. * The logfiles include ui Extension host process id in their log file names. This ensures that filenames from multiple instances of Azure Data Studio running do not collide with each other. Furthermore log directory for being used for the tools service backend processes. This ensures that there is no name conflict when multiple instances of azuredatastudio are running on the same box. Also when azuredatastudio is started from vscode under debugger the log directory is set to %APPDATA%\Code\mssql while the official location is %APPDATA%\azuredatastudio\mssql. So dev environment should not affect other running instances. Kindly note that all debug runs of azuredatastudio share the same directory and all non debug runs share a directory different from those running under debugger. * Log files older than a week get cleaned up upon start-up. The log file cleanup behavior can be controlled at user level by logRetentionMinutes & logFilesRemovalLimit settings. --- .vscode/launch.json | 4 +- ThirdPartyNotices.txt | 1 + extensions/mssql/package.json | 26 ++++++- extensions/mssql/src/constants.ts | 3 +- .../src/credentialstore/credentialstore.ts | 7 +- extensions/mssql/src/main.ts | 13 +--- .../src/resourceProvider/resourceProvider.ts | 6 +- extensions/mssql/src/utils.ts | 72 ++++++++++++++++++- package.json | 1 + .../parts/logs/common/logConstants.ts | 4 +- .../electron-browser/logs.contribution.ts | 14 +++- .../logs/electron-browser/logsActions.ts | 4 +- .../electron-browser/extensionHost.ts | 5 ++ .../electron-browser/extensionService.ts | 13 ++++ yarn.lock | 23 ++++-- 15 files changed, 156 insertions(+), 40 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d8788d99cb..880b5c1ddb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -90,7 +90,7 @@ "**/winjs*.js" ], "webRoot": "${workspaceFolder}", - "timeout": 15000 + "timeout": 45000 }, { "type": "node", @@ -153,4 +153,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 7b4d414763..1ca7809193 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -21,6 +21,7 @@ expressly granted herein, whether by implication, estoppel or otherwise. error-ex: https://github.com/Qix-/node-error-ex escape-string-regexp: https://github.com/sindresorhus/escape-string-regexp fast-plist: https://github.com/Microsoft/node-fast-plist + find-remove: https://www.npmjs.com/package/find-remove fs-extra: https://github.com/jprichardson/node-fs-extra gc-signals: https://github.com/Microsoft/node-gc-signals getmac: https://github.com/bevry/getmac diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 7302c1915a..023e235259 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -93,7 +93,31 @@ "mssql.logDebugInfo": { "type": "boolean", "default": false, - "description": "[Optional] Log debug output to the VS Code console (Help -> Toggle Developer Tools)" + "description": "[Optional] Log debug output to the console (View -> Output) and then select appropriate output channel from the dropdown" + }, + "mssql.tracingLevel": { + "type": "string", + "description": "[Optional] Log level for backend services. Azure Data Studio generates a file name every time it starts and if the file already exists the logs entries are appended to that file. For cleanup of old log files see logRetentionMinutes and logFilesRemovalLimit settings. The default tracingLevel does not log much. Changing verbosity could lead to extensive logging and disk space requirements for the logs. Error includes Critical, Warning includes Error, Information includes Warning and Verbose includes Information", + "default": "Critical", + "enum": [ + "All", + "Off", + "Critical", + "Error", + "Warning", + "Information", + "Verbose" + ] + }, + "mssql.logRetentionMinutes": { + "type": "number", + "default": 10080, + "description": "Number of minutes to retain log files for backend services. Default is 1 week." + }, + "mssql.logFilesRemovalLimit": { + "type": "number", + "default": 100, + "description": "Maximum number of old files to remove upon startup that have expired mssql.logRetentionMinutes. Files that do not get cleaned up due to this limitation get cleaned up next time Azure Data Studio starts up." }, "ignorePlatformWarning": { "type": "boolean", diff --git a/extensions/mssql/src/constants.ts b/extensions/mssql/src/constants.ts index 0503be5bf1..1dc58ff589 100644 --- a/extensions/mssql/src/constants.ts +++ b/extensions/mssql/src/constants.ts @@ -9,5 +9,4 @@ export const providerId = 'MSSQL'; export const serviceCrashMessage = 'SQL Tools Service component exited unexpectedly. Please restart Azure Data Studio.'; export const serviceCrashButton = 'View Known Issues'; export const serviceCrashLink = 'https://github.com/Microsoft/vscode-mssql/wiki/SqlToolsService-Known-Issues'; -export const configLogDebugInfo = 'logDebugInfo'; -export const extensionConfigSectionName = 'mssql'; \ No newline at end of file +export const extensionConfigSectionName = 'mssql'; diff --git a/extensions/mssql/src/credentialstore/credentialstore.ts b/extensions/mssql/src/credentialstore/credentialstore.ts index 247c81f37d..b304551ae7 100644 --- a/extensions/mssql/src/credentialstore/credentialstore.ts +++ b/extensions/mssql/src/credentialstore/credentialstore.ts @@ -10,7 +10,6 @@ import { IConfig, ServerProvider } from 'service-downloader'; import { ServerOptions, RPCMessageType, ClientCapabilities, ServerCapabilities, TransportKind } from 'vscode-languageclient'; import { Disposable } from 'vscode'; import * as UUID from 'vscode-languageclient/lib/utils/uuid'; - import * as sqlops from 'sqlops'; import * as Contracts from './contracts'; @@ -100,11 +99,7 @@ export class CredentialStore { } private generateServerOptions(executablePath: string): ServerOptions { - let launchArgs = []; - launchArgs.push('--log-dir'); - let logFileLocation = path.join(Utils.getDefaultLogLocation(), 'mssql'); - launchArgs.push(logFileLocation); - + let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles('credentialstore', executablePath); return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; } } diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 6261732942..401a6d3b15 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -97,18 +97,7 @@ export async function activate(context: vscode.ExtensionContext) { } function generateServerOptions(executablePath: string): ServerOptions { - let launchArgs = []; - launchArgs.push('--log-dir'); - let logFileLocation = path.join(Utils.getDefaultLogLocation(), 'mssql'); - launchArgs.push(logFileLocation); - let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName); - if (config) { - let logDebugInfo = config[Constants.configLogDebugInfo]; - if (logDebugInfo) { - launchArgs.push('--enable-logging'); - } - } - + let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles('sqltools', executablePath); return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; } diff --git a/extensions/mssql/src/resourceProvider/resourceProvider.ts b/extensions/mssql/src/resourceProvider/resourceProvider.ts index fefe992dae..5cb23770a5 100644 --- a/extensions/mssql/src/resourceProvider/resourceProvider.ts +++ b/extensions/mssql/src/resourceProvider/resourceProvider.ts @@ -105,11 +105,7 @@ export class AzureResourceProvider { } private generateServerOptions(executablePath: string): ServerOptions { - let launchArgs = []; - launchArgs.push('--log-dir'); - let logFileLocation = path.join(Utils.getDefaultLogLocation(), 'mssql'); - launchArgs.push(logFileLocation); - + let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles('resourceprovider', executablePath); return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; } } diff --git a/extensions/mssql/src/utils.ts b/extensions/mssql/src/utils.ts index 0f4f8b932c..3ce734f3c3 100644 --- a/extensions/mssql/src/utils.ts +++ b/extensions/mssql/src/utils.ts @@ -7,6 +7,13 @@ import * as path from 'path'; import * as crypto from 'crypto'; import * as os from 'os'; +import {workspace, WorkspaceConfiguration} from 'vscode'; +import * as findRemoveSync from 'find-remove'; + +const configTracingLevel = 'tracingLevel'; +const configLogRetentionMinutes = 'logRetentionMinutes'; +const configLogFilesRemovalLimit = 'logFilesRemovalLimit'; +const extensionConfigSectionName = 'mssql'; // The function is a duplicate of \src\paths.js. IT would be better to import path.js but it doesn't // work for now because the extension is running in different process. @@ -20,8 +27,69 @@ export function getAppDataPath() { } } -export function getDefaultLogLocation() { - return path.join(getAppDataPath(), 'azuredatastudio'); +export function removeOldLogFiles(prefix: string) : JSON { + return findRemoveSync(getDefaultLogDir(), {prefix: `${prefix}_`, age: {seconds: getConfigLogRetentionSeconds()}, limit: getConfigLogFilesRemovalLimit()}); +} + +export function getConfiguration(config: string = extensionConfigSectionName) : WorkspaceConfiguration { + return workspace.getConfiguration(extensionConfigSectionName); +} + +export function getConfigLogFilesRemovalLimit() : number { + let config = getConfiguration(); + if (config) { + return Number((config[configLogFilesRemovalLimit]).toFixed(0)); + } + else + { + return undefined; + } +} + +export function getConfigLogRetentionSeconds() : number { + let config = getConfiguration(); + if (config) { + return Number((config[configLogRetentionMinutes] * 60).toFixed(0)); + } + else + { + return undefined; + } +} + +export function getConfigTracingLevel() : string { + let config = getConfiguration(); + if (config) { + return config[configTracingLevel]; + } + else + { + return undefined; + } +} + +export function getDefaultLogDir() : string { + return path.join(process.env['VSCODE_LOGS'], '..', '..','mssql'); +} + +export function getDefaultLogFile(prefix: string, pid: number) : string { + return path.join(getDefaultLogDir(), `${prefix}_${pid}.log`); +} + +export function getCommonLaunchArgsAndCleanupOldLogFiles(prefix: string, executablePath: string) : string [] { + let launchArgs = []; + launchArgs.push('--log-file'); + let logFile = getDefaultLogFile(prefix, process.pid); + launchArgs.push(logFile); + + console.log(`logFile for ${path.basename(executablePath)} is ${logFile}`); + console.log(`This process (ui Extenstion Host) is pid: ${process.pid}`); + // Delete old log files + let deletedLogFiles = removeOldLogFiles(prefix); + console.log(`Old log files deletion report: ${JSON.stringify(deletedLogFiles)}`); + launchArgs.push('--tracing-level'); + launchArgs.push(getConfigTracingLevel()); + return launchArgs; } export function ensure(target: object, key: string): any { diff --git a/package.json b/package.json index c79a535016..d9a5e85f50 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "applicationinsights": "0.18.0", "chart.js": "^2.6.0", "fast-plist": "0.1.2", + "find-remove": "1.2.1", "fs-extra": "^3.0.1", "gc-signals": "^0.0.1", "getmac": "1.4.1", diff --git a/src/vs/workbench/parts/logs/common/logConstants.ts b/src/vs/workbench/parts/logs/common/logConstants.ts index f75a511917..8302b6242a 100644 --- a/src/vs/workbench/parts/logs/common/logConstants.ts +++ b/src/vs/workbench/parts/logs/common/logConstants.ts @@ -6,4 +6,6 @@ export const mainLogChannelId = 'mainLog'; export const sharedLogChannelId = 'sharedLog'; export const rendererLogChannelId = 'rendererLog'; -export const extHostLogChannelId = 'extHostLog'; \ No newline at end of file +export const extHostLogChannelId = 'extHostLog'; +// {{SQL CARBON EDIT}} +export const sqlToolsLogChannellId = 'sqlToolsLog'; diff --git a/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts b/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts index c3a9afeee2..6259308590 100644 --- a/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts +++ b/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts @@ -18,14 +18,18 @@ import * as Constants from 'vs/workbench/parts/logs/common/logConstants'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ShowLogsAction, OpenLogsFolderAction, SetLogLevelAction, OpenLogFileAction } from 'vs/workbench/parts/logs/electron-browser/logsActions'; - +// {{SQL CARBON EDIT}} +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionService } from 'vs/workbench/services/extensions/electron-browser/extensionService'; class LogOutputChannels extends Disposable implements IWorkbenchContribution { constructor( @IWindowService private windowService: IWindowService, @IEnvironmentService private environmentService: IEnvironmentService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + // {{SQL CARBON EDIT}} + @IExtensionService private extensionService: ExtensionService ) { super(); let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); @@ -33,6 +37,12 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { outputChannelRegistry.registerChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Log (Shared)"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); outputChannelRegistry.registerChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Log (Window)"), URI.file(join(this.environmentService.logsPath, `renderer${this.windowService.getCurrentWindowId()}.log`))); outputChannelRegistry.registerChannel(Constants.extHostLogChannelId, nls.localize('extensionsLog', "Log (Extension Host)"), URI.file(join(this.environmentService.logsPath, `exthost${this.windowService.getCurrentWindowId()}.log`))); + // {{SQL CARBON EDIT}} + let extHostPid : number = extensionService.getExtenstionHostProcessId(); + console.log(`extensionHost process id is ${extHostPid}`); + let toolsServiceLogFile : string = join(this.environmentService.logsPath, '..', '..', 'mssql', `sqltools_${extHostPid}.log`); + console.log(`SqlTools Log file is: ${toolsServiceLogFile}`); + outputChannelRegistry.registerChannel(Constants.sqlToolsLogChannellId, nls.localize('sqlToolsLog', "Log (SqlTools)"), URI.file(toolsServiceLogFile)); const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); const devCategory = nls.localize('developer', "Developer"); diff --git a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts index 88eb7bfc6e..bab378c18a 100644 --- a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts +++ b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts @@ -52,7 +52,9 @@ export class ShowLogsAction extends Action { { id: Constants.rendererLogChannelId, label: this.contextService.getWorkspace().name ? nls.localize('rendererProcess', "Window ({0})", this.contextService.getWorkspace().name) : nls.localize('emptyWindow', "Window") }, { id: Constants.extHostLogChannelId, label: nls.localize('extensionHost', "Extension Host") }, { id: Constants.sharedLogChannelId, label: nls.localize('sharedProcess', "Shared") }, - { id: Constants.mainLogChannelId, label: nls.localize('mainProcess', "Main") } + { id: Constants.mainLogChannelId, label: nls.localize('mainProcess', "Main") }, + // {{SQL CARBON EDIT}} + { id: Constants.sqlToolsLogChannellId, label: nls.localize('sqlToolsHost', "SqlTools") } ]; return this.quickOpenService.pick(entries, { placeHolder: nls.localize('selectProcess', "Select Log for Process") }) diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index bc9e359308..52b8e6f10e 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -101,6 +101,11 @@ export class ExtensionHostProcessWorker { })); } + // {{SQL CARBON EDIT}} + public getExtenstionHostProcess(): ChildProcess { + return this._extensionHostProcess; + } + public dispose(): void { this.terminate(); } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 24592aab8e..40e0ba7fa8 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -152,6 +152,11 @@ export class ExtensionHostProcessManager extends Disposable { }); } + // {{SQL CARBON EDIT}} + public getExtenstionHostProcessWorker(): ExtensionHostProcessWorker { + return this._extensionHostProcessWorker; + } + public dispose(): void { if (this._extensionHostProcessWorker) { this._extensionHostProcessWorker.dispose(); @@ -296,6 +301,14 @@ export class ExtensionService extends Disposable implements IExtensionService { } } + // {{SQL CARBON EDIT}} + public getExtenstionHostProcessId(): number { + if (this._extensionHostProcessManagers.length !== 1) + { + this._logOrShowMessage(Severity.Warning, 'Exactly one Extension Host Process Manager was expected'); + } + return this._extensionHostProcessManagers[0].getExtenstionHostProcessWorker().getExtenstionHostProcess().pid; + } private startDelayed(lifecycleService: ILifecycleService): void { let started = false; const startOnce = () => { diff --git a/yarn.lock b/yarn.lock index 4406bdfae4..6af0ff8e4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2159,6 +2159,13 @@ find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" +find-remove@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/find-remove/-/find-remove-1.2.1.tgz#afd93400d23890e018ea197591e9d850d3d049a2" + dependencies: + fmerge "1.2.0" + rimraf "2.6.2" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -2225,6 +2232,10 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +fmerge@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fmerge/-/fmerge-1.2.0.tgz#36e99d2ae255e3ee1af666b4df780553671cf692" + for-in@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.5.tgz#007374e2b6d5c67420a1479bdb75a04872b738c4" @@ -5892,15 +5903,15 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@^2.2.8: - version "2.6.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" +rimraf@2.6.2, rimraf@^2.4.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" -rimraf@^2.4.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" +rimraf@^2.2.8: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5"