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); } }