diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index 920057d5ef..88a9399976 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -65,6 +65,16 @@ "title": "%accounts.clearTokenCache%", "category": "Azure Accounts" }, + { + "command": "azure.accounts.getSubscriptions", + "title": "%azure.accounts.getSubscriptions.title%", + "category": "Azure Accounts" + }, + { + "command": "azure.accounts.getResourceGroups", + "title": "%azure.accounts.getResourceGroups.title%", + "category": "Azure Accounts" + }, { "command": "azure.resource.signin", "title": "%azure.resource.signin.title%", @@ -196,6 +206,6 @@ "vscodetestcover": "github:corivera/vscodetestcover#1.0.4" }, "resolutions": { - "esprima": "^4.0.0" + "esprima": "^4.0.0" } } diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json index 00cf99420a..497456ae1a 100644 --- a/extensions/azurecore/package.nls.json +++ b/extensions/azurecore/package.nls.json @@ -14,6 +14,8 @@ "azure.resource.connectsqldb.title": "Add to Servers", "accounts.clearTokenCache": "Clear Azure Account Token Cache", + "azure.accounts.getSubscriptions.title": "Get Azure Account Subscriptions", + "azure.accounts.getResourceGroups.title": "Get Azure Account Subscription Resource Groups", "config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled", "config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled", diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index 056e5e1f50..4db8faab22 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -21,8 +21,30 @@ export namespace azureResource { readonly treeItem: TreeItem; } - export interface AzureResourceSubscription { - id: string; + export interface AzureResource { name: string; + id: string; } + + export interface AzureResourceSubscription extends AzureResource { + } + + export interface AzureSqlResource extends AzureResource { + loginName: string; + } + + export interface AzureResourceResourceGroup extends AzureResource { + } + + export interface AzureResourceDatabase extends AzureSqlResource { + serverName: string; + serverFullName: string; + } + + export interface AzureResourceDatabaseServer extends AzureSqlResource { + fullName: string; + defaultDatabaseName: string; + } + + } diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts index 3d8e0ef71e..6301aca5b8 100644 --- a/extensions/azurecore/src/azureResource/commands.ts +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -18,8 +18,47 @@ import { AzureResourceTreeProvider } from './tree/treeProvider'; import { AzureResourceAccountTreeNode } from './tree/accountTreeNode'; import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../azureResource/interfaces'; import { AzureResourceServiceNames } from './constants'; +import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService'; export function registerAzureResourceCommands(appContext: AppContext, tree: AzureResourceTreeProvider): void { + + // Resource Management commands + appContext.apiWrapper.registerCommand('azure.accounts.getSubscriptions', async (account: azdata.Account) => { + const subscriptions = []; + try { + const subscriptionService = appContext.getService(AzureResourceServiceNames.subscriptionService); + const tokens = await appContext.apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement); + + for (const tenant of account.properties.tenants) { + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; + + subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType))); + } + } catch (error) { + throw new Error(localize('azure.accounts.getSubscriptions.error', "Unexpected error occurred getting the subscriptions for account {0}. {1}", account.key.accountId, error)); + } + return subscriptions; + }); + + appContext.apiWrapper.registerCommand('azure.accounts.getResourceGroups', async (account: azdata.Account, subscription: azureResource.AzureResourceSubscription) => { + try { + const service = new AzureResourceGroupService(); + const resourceGroups: azureResource.AzureResourceResourceGroup[] = []; + for (const tenant of account.properties.tenants) { + const tokens = await appContext.apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement); + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; + + resourceGroups.push(...await service.getResources(subscription, new TokenCredentials(token, tokenType))); + } + return resourceGroups; + } catch (error) { + throw new Error(localize('azure.accounts.getResourceGroups.error', "Unexpected error occurred getting the subscriptions for subscription {0} ({1}). {2}", subscription.name, subscription.id, error)); + } + }); + + // Resource Tree commands appContext.apiWrapper.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => { if (!(node instanceof AzureResourceAccountTreeNode)) { return; diff --git a/extensions/azurecore/src/azureResource/interfaces.ts b/extensions/azurecore/src/azureResource/interfaces.ts index 4b571e256a..4afc2cc5f2 100644 --- a/extensions/azurecore/src/azureResource/interfaces.ts +++ b/extensions/azurecore/src/azureResource/interfaces.ts @@ -41,23 +41,6 @@ export interface IAzureResourceNodeWithProviderId { resourceNode: azureResource.IAzureResourceNode; } -export interface AzureSqlResource { - name: string; - loginName: string; -} - -export interface IAzureResourceService { +export interface IAzureResourceService { getResources(subscription: azureResource.AzureResourceSubscription, credential: msRest.ServiceClientCredentials): Promise; } - - -export interface AzureResourceDatabase extends AzureSqlResource { - serverName: string; - serverFullName: string; -} - -export interface AzureResourceDatabaseServer extends AzureSqlResource { - id?: string; - fullName: string; - defaultDatabaseName: string; -} diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts b/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts index 6fad11c92c..f63f4e5d99 100644 --- a/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/database/databaseProvider.ts @@ -9,11 +9,11 @@ import { ApiWrapper } from '../../../apiWrapper'; import { azureResource } from '../../azure-resource'; import { AzureResourceDatabaseTreeDataProvider } from './databaseTreeDataProvider'; -import { IAzureResourceService, AzureResourceDatabase } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; export class AzureResourceDatabaseProvider implements azureResource.IAzureResourceProvider { public constructor( - private _databaseService: IAzureResourceService, + private _databaseService: IAzureResourceService, private _apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseService.ts b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts index 6864832407..f31f81a900 100644 --- a/extensions/azurecore/src/azureResource/providers/database/databaseService.ts +++ b/extensions/azurecore/src/azureResource/providers/database/databaseService.ts @@ -5,7 +5,7 @@ import { ServiceClientCredentials } from '@azure/ms-rest-js'; import { azureResource } from '../../azure-resource'; -import { IAzureResourceService, AzureResourceDatabase } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; import { serversQuery, DbServerGraphData } from '../databaseServer/databaseServerService'; import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { queryGraphResources, GraphData } from '../resourceTreeDataProviderBase'; @@ -13,9 +13,9 @@ import { queryGraphResources, GraphData } from '../resourceTreeDataProviderBase' interface DatabaseGraphData extends GraphData { kind: string; } -export class AzureResourceDatabaseService implements IAzureResourceService { - public async getResources(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise { - const databases: AzureResourceDatabase[] = []; +export class AzureResourceDatabaseService implements IAzureResourceService { + public async getResources(subscription: azureResource.AzureResourceSubscription, credential: ServiceClientCredentials): Promise { + const databases: azureResource.AzureResourceDatabase[] = []; const resourceClient = new ResourceGraphClient(credential); // Query servers and databases in parallel (start both promises before waiting on the 1st) @@ -46,6 +46,7 @@ export class AzureResourceDatabaseService implements IAzureResourceService { +export class AzureResourceDatabaseTreeDataProvider extends ResourceTreeDataProviderBase { private static readonly containerId = 'azure.resource.providers.database.treeDataProvider.databaseContainer'; private static readonly containerLabel = localize('azure.resource.providers.database.treeDataProvider.databaseContainerLabel', "SQL Databases"); public constructor( - databaseService: IAzureResourceService, + databaseService: IAzureResourceService, apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { super(databaseService, apiWrapper); } - protected getTreeItemForResource(database: AzureResourceDatabase): TreeItem { + protected getTreeItemForResource(database: azureResource.AzureResourceDatabase): TreeItem { return { id: `databaseServer_${database.serverFullName}.database_${database.name}`, label: `${database.name} (${database.serverName})`, diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerProvider.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerProvider.ts index ad552763c8..832e276c93 100644 --- a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerProvider.ts @@ -7,12 +7,12 @@ import { ExtensionContext } from 'vscode'; import { ApiWrapper } from '../../../apiWrapper'; import { azureResource } from '../../azure-resource'; -import { IAzureResourceService, AzureResourceDatabaseServer } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; import { AzureResourceDatabaseServerTreeDataProvider } from './databaseServerTreeDataProvider'; export class AzureResourceDatabaseServerProvider implements azureResource.IAzureResourceProvider { public constructor( - private _databaseServerService: IAzureResourceService, + private _databaseServerService: IAzureResourceService, private _apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts index aadb05c1bc..0694e09014 100644 --- a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerService.ts @@ -5,7 +5,7 @@ import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; -import { AzureResourceDatabaseServer } from '../../interfaces'; +import { azureResource } from '../../azure-resource'; export interface DbServerGraphData extends GraphData { @@ -17,13 +17,13 @@ export interface DbServerGraphData extends GraphData { export const serversQuery = 'where type == "microsoft.sql/servers"'; -export class AzureResourceDatabaseServerService extends ResourceServiceBase { +export class AzureResourceDatabaseServerService extends ResourceServiceBase { protected get query(): string { return serversQuery; } - protected convertResource(resource: DbServerGraphData): AzureResourceDatabaseServer { + protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceDatabaseServer { return { id: resource.id, name: resource.name, diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts index 340585a018..6595c518f6 100644 --- a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts @@ -11,16 +11,16 @@ const localize = nls.loadMessageBundle(); import { AzureResourceItemType } from '../../../azureResource/constants'; import { ApiWrapper } from '../../../apiWrapper'; import { generateGuid } from '../../utils'; -import { IAzureResourceService, AzureResourceDatabaseServer } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; import { ResourceTreeDataProviderBase } from '../resourceTreeDataProviderBase'; import { azureResource } from '../../azure-resource'; -export class AzureResourceDatabaseServerTreeDataProvider extends ResourceTreeDataProviderBase { +export class AzureResourceDatabaseServerTreeDataProvider extends ResourceTreeDataProviderBase { private static readonly containerId = 'azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainer'; private static readonly containerLabel = localize('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainerLabel', "SQL Servers"); public constructor( - databaseServerService: IAzureResourceService, + databaseServerService: IAzureResourceService, apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { @@ -28,7 +28,7 @@ export class AzureResourceDatabaseServerTreeDataProvider extends ResourceTreeDat } - protected getTreeItemForResource(databaseServer: AzureResourceDatabaseServer): TreeItem { + protected getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer): TreeItem { return { id: `databaseServer_${databaseServer.id ? databaseServer.id : databaseServer.name}`, label: databaseServer.name, diff --git a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerProvider.ts b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerProvider.ts index d3b79954a0..21b248874f 100644 --- a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerProvider.ts @@ -7,12 +7,12 @@ import { ExtensionContext } from 'vscode'; import { ApiWrapper } from '../../../apiWrapper'; import { azureResource } from '../../azure-resource'; -import { IAzureResourceService, AzureResourceDatabaseServer } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; import { PostgresServerTreeDataProvider as PostgresServerTreeDataProvider } from './postgresServerTreeDataProvider'; export class PostgresServerProvider implements azureResource.IAzureResourceProvider { public constructor( - private _databaseServerService: IAzureResourceService, + private _databaseServerService: IAzureResourceService, private _apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { diff --git a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts index df50f734fc..41230d78ea 100644 --- a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts +++ b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerService.ts @@ -5,7 +5,7 @@ import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; -import { AzureResourceDatabaseServer } from '../../interfaces'; +import { azureResource } from '../../azure-resource'; interface DbServerGraphData extends GraphData { @@ -17,13 +17,13 @@ interface DbServerGraphData extends GraphData { const serversQuery = 'where type == "microsoft.dbforpostgresql/servers"'; -export class PostgresServerService extends ResourceServiceBase { +export class PostgresServerService extends ResourceServiceBase { protected get query(): string { return serversQuery; } - protected convertResource(resource: DbServerGraphData): AzureResourceDatabaseServer { + protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceDatabaseServer { return { id: resource.id, name: resource.name, diff --git a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts index 64a851ef0c..525fecd2dd 100644 --- a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts @@ -11,16 +11,16 @@ const localize = nls.loadMessageBundle(); import { AzureResourceItemType } from '../../constants'; import { ApiWrapper } from '../../../apiWrapper'; import { generateGuid } from '../../utils'; -import { IAzureResourceService, AzureResourceDatabaseServer } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; import { ResourceTreeDataProviderBase } from '../resourceTreeDataProviderBase'; import { azureResource } from '../../azure-resource'; -export class PostgresServerTreeDataProvider extends ResourceTreeDataProviderBase { +export class PostgresServerTreeDataProvider extends ResourceTreeDataProviderBase { private static readonly containerId = 'azure.resource.providers.databaseServer.treeDataProvider.postgresServerContainer'; private static readonly containerLabel = localize('azure.resource.providers.databaseServer.treeDataProvider.postgresServerContainerLabel', "Azure Database for PostgreSQL Servers"); public constructor( - databaseServerService: IAzureResourceService, + databaseServerService: IAzureResourceService, apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { @@ -28,7 +28,7 @@ export class PostgresServerTreeDataProvider extends ResourceTreeDataProviderBase } - protected getTreeItemForResource(databaseServer: AzureResourceDatabaseServer): TreeItem { + protected getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer): TreeItem { return { id: `databaseServer_${databaseServer.id ? databaseServer.id : databaseServer.name}`, label: databaseServer.name, diff --git a/extensions/azurecore/src/azureResource/providers/resourceGroup/resourceGroupService.ts b/extensions/azurecore/src/azureResource/providers/resourceGroup/resourceGroupService.ts new file mode 100644 index 0000000000..8220f9f9a5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/resourceGroup/resourceGroupService.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DbServerGraphData } from '../databaseServer/databaseServerService'; +import { azureResource } from '../../azure-resource'; +import { ResourceServiceBase } from '../resourceTreeDataProviderBase'; + +export class AzureResourceGroupService extends ResourceServiceBase { + + protected get query(): string { + return 'ResourceContainers | where type=="microsoft.resources/subscriptions/resourcegroups"'; + } + + protected convertResource(resource: DbServerGraphData): azureResource.AzureResourceResourceGroup { + return { + id: resource.id, + name: resource.name + }; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts b/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts index 195f985742..a77e179464 100644 --- a/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts +++ b/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts @@ -3,16 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureResource, TreeItem } from 'azdata'; +import * as azdata from 'azdata'; import * as msRest from '@azure/ms-rest-js'; import { azureResource } from '../azure-resource'; import { ApiWrapper } from '../../apiWrapper'; -import { IAzureResourceService, AzureSqlResource } from '../interfaces'; +import { IAzureResourceService } from '../interfaces'; import { AzureResourceErrorMessageUtil } from '../utils'; import { ResourceGraphClient } from '@azure/arm-resourcegraph'; -export abstract class ResourceTreeDataProviderBase implements azureResource.IAzureResourceTreeDataProvider { +export abstract class ResourceTreeDataProviderBase implements azureResource.IAzureResourceTreeDataProvider { public constructor( protected _resourceService: IAzureResourceService, @@ -20,7 +20,7 @@ export abstract class ResourceTreeDataProviderBase i ) { } - public getTreeItem(element: azureResource.IAzureResourceNode): TreeItem | Thenable { + public getTreeItem(element: azureResource.IAzureResourceNode): azdata.TreeItem | Thenable { return element.treeItem; } @@ -45,14 +45,14 @@ export abstract class ResourceTreeDataProviderBase i } private async getResources(element: azureResource.IAzureResourceNode): Promise { - const tokens = await this._apiWrapper.getSecurityToken(element.account, AzureResource.ResourceManagement); + const tokens = await this._apiWrapper.getSecurityToken(element.account, azdata.AzureResource.ResourceManagement); const credential = new msRest.TokenCredentials(tokens[element.tenantId].token, tokens[element.tenantId].tokenType); const resources: T[] = await this._resourceService.getResources(element.subscription, credential) || []; return resources; } - protected abstract getTreeItemForResource(resource: T): TreeItem; + protected abstract getTreeItemForResource(resource: T): azdata.TreeItem; protected abstract createContainerNode(): azureResource.IAzureResourceNode; } @@ -89,10 +89,14 @@ export async function queryGraphResources(resourceClient: R return allResources; } -export abstract class ResourceServiceBase implements IAzureResourceService { +export abstract class ResourceServiceBase implements IAzureResourceService { constructor() { } + /** + * The query to use - see https://docs.microsoft.com/azure/governance/resource-graph/concepts/query-language + * for more information on the supported syntax and tables/properties + */ protected abstract get query(): string; public async getResources(subscription: azureResource.AzureResourceSubscription, credential: msRest.ServiceClientCredentials): Promise { diff --git a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceProvider.ts b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceProvider.ts index ec3ea673fe..09b4e7c8d8 100644 --- a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceProvider.ts @@ -7,12 +7,12 @@ import { ExtensionContext } from 'vscode'; import { ApiWrapper } from '../../../apiWrapper'; import { azureResource } from '../../azure-resource'; -import { IAzureResourceService, AzureResourceDatabaseServer } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; import { SqlInstanceTreeDataProvider as SqlInstanceTreeDataProvider } from './sqlInstanceTreeDataProvider'; export class SqlInstanceProvider implements azureResource.IAzureResourceProvider { public constructor( - private _service: IAzureResourceService, + private _service: IAzureResourceService, private _apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { diff --git a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts index f5f01f1219..a45f3b985b 100644 --- a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts +++ b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureResourceDatabaseServer } from '../../interfaces'; +import { azureResource } from '../../azure-resource'; import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; interface SqlInstanceGraphData extends GraphData { @@ -15,13 +15,13 @@ interface SqlInstanceGraphData extends GraphData { const instanceQuery = 'where type == "microsoft.sql/managedinstances"'; -export class SqlInstanceResourceService extends ResourceServiceBase { +export class SqlInstanceResourceService extends ResourceServiceBase { protected get query(): string { return instanceQuery; } - protected convertResource(resource: SqlInstanceGraphData): AzureResourceDatabaseServer { + protected convertResource(resource: SqlInstanceGraphData): azureResource.AzureResourceDatabaseServer { return { id: resource.id, name: resource.name, diff --git a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts index a4942d0fea..72fc994c21 100644 --- a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts @@ -11,16 +11,16 @@ const localize = nls.loadMessageBundle(); import { AzureResourceItemType } from '../../constants'; import { ApiWrapper } from '../../../apiWrapper'; import { generateGuid } from '../../utils'; -import { IAzureResourceService, AzureResourceDatabaseServer } from '../../interfaces'; +import { IAzureResourceService } from '../../interfaces'; import { ResourceTreeDataProviderBase } from '../resourceTreeDataProviderBase'; import { azureResource } from '../../azure-resource'; -export class SqlInstanceTreeDataProvider extends ResourceTreeDataProviderBase { +export class SqlInstanceTreeDataProvider extends ResourceTreeDataProviderBase { private static readonly containerId = 'azure.resource.providers.sqlInstanceContainer'; private static readonly containerLabel = localize('azure.resource.providers.sqlInstanceContainerLabel', "SQL Managed Instances"); public constructor( - databaseServerService: IAzureResourceService, + databaseServerService: IAzureResourceService, apiWrapper: ApiWrapper, private _extensionContext: ExtensionContext ) { @@ -28,7 +28,7 @@ export class SqlInstanceTreeDataProvider extends ResourceTreeDataProviderBase>; +let mockDatabaseService: TypeMoq.IMock>; let mockApiWrapper: TypeMoq.IMock; let mockExtensionContext: TypeMoq.IMock; @@ -62,15 +62,17 @@ mockTokens[mockTenantId] = { tokenType: 'Bearer' }; -const mockDatabases: AzureResourceDatabase[] = [ +const mockDatabases: azureResource.AzureResourceDatabase[] = [ { name: 'mock database 1', + id: 'mock-id-1', serverName: 'mock database server 1', serverFullName: 'mock database server full name 1', loginName: 'mock login' }, { name: 'mock database 2', + id: 'mock-id-2', serverName: 'mock database server 2', serverFullName: 'mock database server full name 2', loginName: 'mock login' @@ -79,7 +81,7 @@ const mockDatabases: AzureResourceDatabase[] = [ describe('AzureResourceDatabaseTreeDataProvider.info', function (): void { beforeEach(() => { - mockDatabaseService = TypeMoq.Mock.ofType>(); + mockDatabaseService = TypeMoq.Mock.ofType>(); mockApiWrapper = TypeMoq.Mock.ofType(); mockExtensionContext = TypeMoq.Mock.ofType(); }); @@ -97,7 +99,7 @@ describe('AzureResourceDatabaseTreeDataProvider.info', function (): void { describe('AzureResourceDatabaseTreeDataProvider.getChildren', function (): void { beforeEach(() => { - mockDatabaseService = TypeMoq.Mock.ofType>(); + mockDatabaseService = TypeMoq.Mock.ofType>(); mockApiWrapper = TypeMoq.Mock.ofType(); mockExtensionContext = TypeMoq.Mock.ofType(); diff --git a/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts b/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts index 6610e57509..f6215f7024 100644 --- a/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts +++ b/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts @@ -13,10 +13,10 @@ import { azureResource } from '../../../../azureResource/azure-resource'; import { ApiWrapper } from '../../../../apiWrapper'; import { AzureResourceDatabaseServerTreeDataProvider } from '../../../../azureResource/providers/databaseServer/databaseServerTreeDataProvider'; import { AzureResourceItemType } from '../../../../azureResource/constants'; -import { IAzureResourceService, AzureResourceDatabaseServer } from '../../../../azureResource/interfaces'; +import { IAzureResourceService } from '../../../../azureResource/interfaces'; // Mock services -let mockDatabaseServerService: TypeMoq.IMock>; +let mockDatabaseServerService: TypeMoq.IMock>; let mockApiWrapper: TypeMoq.IMock; let mockExtensionContext: TypeMoq.IMock; @@ -62,15 +62,17 @@ mockTokens[mockTenantId] = { tokenType: 'Bearer' }; -const mockDatabaseServers: AzureResourceDatabaseServer[] = [ +const mockDatabaseServers: azureResource.AzureResourceDatabaseServer[] = [ { name: 'mock database server 1', + id: 'mock-id-1', fullName: 'mock database server full name 1', loginName: 'mock login', defaultDatabaseName: 'master' }, { name: 'mock database server 2', + id: 'mock-id-2', fullName: 'mock database server full name 2', loginName: 'mock login', defaultDatabaseName: 'master' @@ -79,7 +81,7 @@ const mockDatabaseServers: AzureResourceDatabaseServer[] = [ describe('AzureResourceDatabaseServerTreeDataProvider.info', function (): void { beforeEach(() => { - mockDatabaseServerService = TypeMoq.Mock.ofType>(); + mockDatabaseServerService = TypeMoq.Mock.ofType>(); mockApiWrapper = TypeMoq.Mock.ofType(); mockExtensionContext = TypeMoq.Mock.ofType(); }); @@ -97,7 +99,7 @@ describe('AzureResourceDatabaseServerTreeDataProvider.info', function (): void { describe('AzureResourceDatabaseServerTreeDataProvider.getChildren', function (): void { beforeEach(() => { - mockDatabaseServerService = TypeMoq.Mock.ofType>(); + mockDatabaseServerService = TypeMoq.Mock.ofType>(); mockApiWrapper = TypeMoq.Mock.ofType(); mockExtensionContext = TypeMoq.Mock.ofType(); @@ -139,7 +141,7 @@ describe('AzureResourceDatabaseServerTreeDataProvider.getChildren', function (): should(child.account).equal(mockAccount); should(child.subscription).equal(mockSubscription); should(child.tenantId).equal(mockTenantId); - should(child.treeItem.id).equal(`databaseServer_${databaseServer.name}`); + should(child.treeItem.id).equal(`databaseServer_${databaseServer.id}`); should(child.treeItem.label).equal(databaseServer.name); should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); should(child.treeItem.contextValue).equal(AzureResourceItemType.databaseServer); diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 53f2852738..3e39785cd5 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -172,6 +172,11 @@ export interface FieldInfo { editable?: boolean; // for editable dropdown } +export interface AzureAccountFieldInfo extends FieldInfo { + subscriptionVariableName?: string; + resourceGroupVariableName?: string; +} + export const enum LabelPosition { Top = 'top', Left = 'left' @@ -195,7 +200,8 @@ export enum FieldType { Password = 'password', Options = 'options', ReadonlyText = 'readonly_text', - Checkbox = 'checkbox' + Checkbox = 'checkbox', + AzureAccount = 'azure_account' } export interface NotebookInfo { diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts new file mode 100644 index 0000000000..2bff4195cf --- /dev/null +++ b/extensions/resource-deployment/src/localizedConstants.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. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export const account = localize('azure.account', "Azure Account"); +export const subscription = localize('azure.account.subscription', "Subscription"); +export const resourceGroup = localize('azure.account.resourceGroup', "Resource Group"); diff --git a/extensions/resource-deployment/src/typings/ref.d.ts b/extensions/resource-deployment/src/typings/ref.d.ts index cfdf5dd135..73d0439254 100644 --- a/extensions/resource-deployment/src/typings/ref.d.ts +++ b/extensions/resource-deployment/src/typings/ref.d.ts @@ -6,4 +6,5 @@ /// /// /// +/// /// diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts index 80059418e3..d5cf9fe291 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts @@ -137,7 +137,7 @@ export class AzureSettingsPage extends WizardPageBase { self.wizard.registerDisposable(disposable); }, onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent): void => { - self.inputComponents[name] = component; + self.inputComponents[name] = { component: component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts index 60608f9bb0..60466fae2d 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts @@ -198,7 +198,7 @@ export class ClusterSettingsPage extends WizardPageBase { self.wizard.registerDisposable(disposable); }, onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { - self.inputComponents[name] = component; + self.inputComponents[name] = { component: component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); @@ -212,7 +212,7 @@ export class ClusterSettingsPage extends WizardPageBase { self.wizard.registerDisposable(disposable); }, onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { - self.inputComponents[name] = component; + self.inputComponents[name] = { component: component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); @@ -226,7 +226,7 @@ export class ClusterSettingsPage extends WizardPageBase { self.wizard.registerDisposable(disposable); }, onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { - self.inputComponents[name] = component; + self.inputComponents[name] = { component: component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); @@ -235,7 +235,7 @@ export class ClusterSettingsPage extends WizardPageBase { const basicSettingsFormItem = { title: '', component: basicSettingsGroup }; const dockerSettingsFormItem = { title: '', component: dockerSettingsGroup }; this.activeDirectorySection = { title: '', component: activeDirectorySettingsGroup }; - const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName]; + const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName].component; this.formBuilder = view.modelBuilder.formContainer().withFormItems( [basicSettingsFormItem, dockerSettingsFormItem], { @@ -290,7 +290,7 @@ export class ClusterSettingsPage extends WizardPageBase { getInputBoxComponent(VariableNames.DockerRegistry_VariableName, this.inputComponents).value = this.wizard.model.getStringValue(VariableNames.DockerRegistry_VariableName); getInputBoxComponent(VariableNames.DockerRepository_VariableName, this.inputComponents).value = this.wizard.model.getStringValue(VariableNames.DockerRepository_VariableName); getInputBoxComponent(VariableNames.DockerImageTag_VariableName, this.inputComponents).value = this.wizard.model.getStringValue(VariableNames.DockerImageTag_VariableName); - const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName]; + const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName].component; if (authModeDropdown) { authModeDropdown.enabled = this.wizard.model.adAuthSupported; const adAuthSelected = (authModeDropdown.value).name === 'ad'; diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts index 295128c608..ec695f8e3b 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts @@ -296,7 +296,7 @@ export class ServiceSettingsPage extends WizardPageBase { this.wizard.registerDisposable(disposable); }, onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { - this.inputComponents[name] = component; + this.inputComponents[name] = { component: component }; }, onNewValidatorCreated: (validator: Validator): void => { } @@ -348,43 +348,43 @@ export class ServiceSettingsPage extends WizardPageBase { this.controllerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth }); this.controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: NumberInputWidth, min: 1 }); this.controllerEndpointRow = createFlexContainer(view, [this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput]); - this.inputComponents[VariableNames.ControllerDNSName_VariableName] = this.controllerDNSInput; - this.inputComponents[VariableNames.ControllerPort_VariableName] = this.controllerPortInput; + this.inputComponents[VariableNames.ControllerDNSName_VariableName] = { component: this.controllerDNSInput }; + this.inputComponents[VariableNames.ControllerPort_VariableName] = { component: this.controllerPortInput }; this.SqlServerNameLabel = createLabel(view, { text: localize('deployCluster.MasterSqlText', "SQL Server Master"), width: labelWidth, required: true }); this.sqlServerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth }); this.sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: NumberInputWidth, min: 1 }); this.sqlServerEndpointRow = createFlexContainer(view, [this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput]); - this.inputComponents[VariableNames.SQLServerDNSName_VariableName] = this.sqlServerDNSInput; - this.inputComponents[VariableNames.SQLServerPort_VariableName] = this.sqlServerPortInput; + this.inputComponents[VariableNames.SQLServerDNSName_VariableName] = { component: this.sqlServerDNSInput }; + this.inputComponents[VariableNames.SQLServerPort_VariableName] = { component: this.sqlServerPortInput }; this.gatewayNameLabel = createLabel(view, { text: localize('deployCluster.GatewayText', "Gateway"), width: labelWidth, required: true }); this.gatewayDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth }); this.gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: NumberInputWidth, min: 1 }); this.gatewayEndpointRow = createFlexContainer(view, [this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput]); - this.inputComponents[VariableNames.GatewayDNSName_VariableName] = this.gatewayDNSInput; - this.inputComponents[VariableNames.GateWayPort_VariableName] = this.gatewayPortInput; + this.inputComponents[VariableNames.GatewayDNSName_VariableName] = { component: this.gatewayDNSInput }; + this.inputComponents[VariableNames.GateWayPort_VariableName] = { component: this.gatewayPortInput }; this.serviceProxyNameLabel = createLabel(view, { text: localize('deployCluster.ServiceProxyText', "Management proxy"), width: labelWidth, required: true }); this.serviceProxyDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ServiceProxyDNSName', "Management proxy DNS name"), required: false, width: inputWidth }); this.serviceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ServiceProxyPortName', "Management proxy port"), required: true, width: NumberInputWidth, min: 1 }); this.serviceProxyEndpointRow = createFlexContainer(view, [this.serviceProxyNameLabel, this.serviceProxyDNSInput, this.serviceProxyPortInput]); - this.inputComponents[VariableNames.ServiceProxyDNSName_VariableName] = this.serviceProxyDNSInput; - this.inputComponents[VariableNames.ServiceProxyPort_VariableName] = this.serviceProxyPortInput; + this.inputComponents[VariableNames.ServiceProxyDNSName_VariableName] = { component: this.serviceProxyDNSInput }; + this.inputComponents[VariableNames.ServiceProxyPort_VariableName] = { component: this.serviceProxyPortInput }; this.appServiceProxyNameLabel = createLabel(view, { text: localize('deployCluster.AppServiceProxyText', "Application proxy"), width: labelWidth, required: true }); this.appServiceProxyDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyDNSName', "Application proxy DNS name"), required: false, width: inputWidth }); this.appServiceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyPortName', "Application proxy port"), required: true, width: NumberInputWidth, min: 1 }); this.appServiceProxyEndpointRow = createFlexContainer(view, [this.appServiceProxyNameLabel, this.appServiceProxyDNSInput, this.appServiceProxyPortInput]); - this.inputComponents[VariableNames.AppServiceProxyDNSName_VariableName] = this.appServiceProxyDNSInput; - this.inputComponents[VariableNames.AppServiceProxyPort_VariableName] = this.appServiceProxyPortInput; + this.inputComponents[VariableNames.AppServiceProxyDNSName_VariableName] = { component: this.appServiceProxyDNSInput }; + this.inputComponents[VariableNames.AppServiceProxyPort_VariableName] = { component: this.appServiceProxyPortInput }; this.readableSecondaryNameLabel = createLabel(view, { text: localize('deployCluster.ReadableSecondaryText', "Readable secondary"), width: labelWidth, required: true }); this.readableSecondaryDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth }); this.readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: NumberInputWidth, min: 1 }); this.readableSecondaryEndpointRow = createFlexContainer(view, [this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput]); - this.inputComponents[VariableNames.ReadableSecondaryDNSName_VariableName] = this.readableSecondaryDNSInput; - this.inputComponents[VariableNames.ReadableSecondaryPort_VariableName] = this.readableSecondaryPortInput; + this.inputComponents[VariableNames.ReadableSecondaryDNSName_VariableName] = { component: this.readableSecondaryDNSInput }; + this.inputComponents[VariableNames.ReadableSecondaryPort_VariableName] = { component: this.readableSecondaryPortInput }; return createGroupContainer(view, [this.endpointHeaderRow, this.controllerEndpointRow, this.sqlServerEndpointRow, this.gatewayEndpointRow, this.serviceProxyEndpointRow, this.appServiceProxyEndpointRow, this.readableSecondaryEndpointRow], { header: localize('deployCluster.EndpointSettings', "Endpoint settings"), diff --git a/extensions/resource-deployment/src/ui/deploymentInputDialog.ts b/extensions/resource-deployment/src/ui/deploymentInputDialog.ts index 7c206669fa..a3120bf71a 100644 --- a/extensions/resource-deployment/src/ui/deploymentInputDialog.ts +++ b/extensions/resource-deployment/src/ui/deploymentInputDialog.ts @@ -9,7 +9,7 @@ import * as nls from 'vscode-nls'; import { DialogBase } from './dialogBase'; import { INotebookService } from '../services/notebookService'; import { DialogInfo, instanceOfNotebookBasedDialogInfo, NotebookBasedDialogInfo } from '../interfaces'; -import { Validator, initializeDialog, InputComponents, setModelValues } from './modelViewUtils'; +import { Validator, initializeDialog, InputComponents, setModelValues, InputValueTransformer } from './modelViewUtils'; import { Model } from './model'; import { EOL } from 'os'; import { getDateTimeString, getErrorMessage } from '../utils'; @@ -46,8 +46,8 @@ export class DeploymentInputDialog extends DialogBase { onNewDisposableCreated: (disposable: vscode.Disposable): void => { this._toDispose.push(disposable); }, - onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { - this.inputComponents[name] = component; + onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent, inputValueTransformer?: InputValueTransformer): void => { + this.inputComponents[name] = { component: component, inputValueTransformer: inputValueTransformer }; }, onNewValidatorCreated: (validator: Validator): void => { validators.push(validator); diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 9622b7af9e..824e7f38c6 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -6,25 +6,28 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { DialogInfoBase, FieldType, FieldInfo, SectionInfo, LabelPosition, FontWeight, FontStyle } from '../interfaces'; +import { DialogInfoBase, FieldType, FieldInfo, SectionInfo, LabelPosition, FontWeight, FontStyle, AzureAccountFieldInfo } from '../interfaces'; import { Model } from './model'; import { getDateTimeString } from '../utils'; +import { azureResource } from '../../../azurecore/src/azureResource/azure-resource'; +import * as loc from '../localizedConstants'; const localize = nls.loadMessageBundle(); export type Validator = () => { valid: boolean, message: string }; -export type InputComponents = { [s: string]: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent; }; +export type InputValueTransformer = (inputValue: string) => string; +export type InputComponents = { [s: string]: { component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent; inputValueTransformer?: InputValueTransformer } }; export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent { - return inputComponents[name]; + return inputComponents[name].component; } export function getDropdownComponent(name: string, inputComponents: InputComponents): azdata.DropDownComponent { - return inputComponents[name]; + return inputComponents[name].component; } export function getCheckboxComponent(name: string, inputComponents: InputComponents): azdata.CheckBoxComponent { - return inputComponents[name]; + return inputComponents[name].component; } export const DefaultInputComponentWidth = '400px'; @@ -52,11 +55,15 @@ interface FieldContext extends CreateContext { view: azdata.ModelView; } +interface AzureAccountFieldContext extends FieldContext { + fieldInfo: AzureAccountFieldInfo; +} + interface CreateContext { container: azdata.window.Dialog | azdata.window.Wizard; onNewValidatorCreated: (validator: Validator) => void; onNewDisposableCreated: (disposable: vscode.Disposable) => void; - onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent) => void; + onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent, inputValueTransformer?: InputValueTransformer) => void; } export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValue?: string, ariaLabel: string, required?: boolean, placeHolder?: string, width?: string }): azdata.InputBoxComponent { @@ -260,6 +267,9 @@ function processField(context: FieldContext): void { case FieldType.Checkbox: processCheckboxField(context); break; + case FieldType.AzureAccount: + processAzureAccountField(context); + break; default: throw new Error(localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", context.fieldInfo.type)); } @@ -411,6 +421,119 @@ function processCheckboxField(context: FieldContext): void { context.onNewInputComponentCreated(context.fieldInfo.variableName!, checkbox); } +// Values used for the Azure Account field inputs +let selectedAccount: azdata.Account | undefined; +let subscriptionDropdown: azdata.DropDownComponent; +let resourceGroupDropdown: azdata.DropDownComponent; +const accountValueToAccountMap = new Map(); +const subscriptionValueToSubscriptionMap = new Map(); + +/** + * An Azure Account field consists of 3 separate dropdown fields - Account, Subscription and Resource Group + * @param context The context to use to create the field + */ +function processAzureAccountField(context: AzureAccountFieldContext): void { + if (subscriptionDropdown) { + throw new Error(localize('onlyOneAzureAccountField', "Only one Azure Account field is supported at this time")); + } + const accountDropdown = createAzureAccountDropdown(context); + createAzureSubscriptionDropdown(context, accountDropdown); + createAzureResourceGroupsDropdown(context, subscriptionDropdown); + azdata.accounts.getAllAccounts().then((accounts: azdata.Account[]) => { + accountDropdown.values = accounts.map(account => { + const displayName = `${account.displayInfo.displayName} (${account.displayInfo.userId})`; + accountValueToAccountMap.set(displayName, account); + return displayName; + }); + selectedAccount = accounts.length > 0 ? accounts[0] : undefined; + handleSelectedAccountChanged(); + }, (err: any) => console.log(`Unexpected error fetching accounts: ${err}`)); +} + +function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.DropDownComponent { + const label = createLabel(context.view, { + text: loc.account, + description: context.fieldInfo.description, + required: context.fieldInfo.required, + width: context.fieldInfo.labelWidth, + fontWeight: context.fieldInfo.labelFontWeight + }); + const accountDropdown = createDropdown(context.view, { + width: context.fieldInfo.inputWidth, + editable: false, + required: context.fieldInfo.required, + label: loc.account + }); + context.onNewInputComponentCreated('', accountDropdown); + addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo.labelPosition); + return accountDropdown; +} + +function createAzureSubscriptionDropdown(context: AzureAccountFieldContext, accountDropdown: azdata.DropDownComponent): void { + const label = createLabel(context.view, { + text: loc.subscription, + required: context.fieldInfo.required, + width: context.fieldInfo.labelWidth, + fontWeight: context.fieldInfo.labelFontWeight + }); + subscriptionDropdown = createDropdown(context.view, { + width: context.fieldInfo.inputWidth, + editable: false, + required: context.fieldInfo.required, + label: loc.subscription + }); + context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName!, subscriptionDropdown, (inputValue: string) => { + return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue; + }); + addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo.labelPosition); + accountDropdown.onValueChanged(selectedItem => { + selectedAccount = accountValueToAccountMap.get(selectedItem.selected); + handleSelectedAccountChanged(); + }); +} + +function handleSelectedAccountChanged(): void { + subscriptionValueToSubscriptionMap.clear(); + subscriptionDropdown.values = []; + vscode.commands.executeCommand('azure.accounts.getSubscriptions', selectedAccount).then(subscriptions => { + subscriptionDropdown.values = (subscriptions).map(subscription => { + const displayName = `${subscription.name} (${subscription.id})`; + subscriptionValueToSubscriptionMap.set(displayName, subscription); + return displayName; + }).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase())); + const selectedSubscription = subscriptionDropdown.values.length > 0 ? subscriptionValueToSubscriptionMap.get(subscriptionDropdown.values[0]) : undefined; + handleSelectedSubscriptionChanged(selectedSubscription); + }, err => { console.log(`Unexpected error fetching subscriptions for account ${selectedAccount?.displayInfo.displayName} (${selectedAccount?.key.accountId}): ${err}`); }); +} + +function createAzureResourceGroupsDropdown(context: AzureAccountFieldContext, subscriptionDropdown: azdata.DropDownComponent): void { + const label = createLabel(context.view, { + text: loc.resourceGroup, + required: context.fieldInfo.required, + width: context.fieldInfo.labelWidth, + fontWeight: context.fieldInfo.labelFontWeight + }); + resourceGroupDropdown = createDropdown(context.view, { + width: context.fieldInfo.inputWidth, + editable: false, + required: context.fieldInfo.required, + label: loc.resourceGroup + }); + context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName!, resourceGroupDropdown); + addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown, context.fieldInfo.labelPosition); + subscriptionDropdown.onValueChanged(selectedItem => { + const selectedSubscription = subscriptionValueToSubscriptionMap.get(selectedItem.selected); + handleSelectedSubscriptionChanged(selectedSubscription); + }); +} + +function handleSelectedSubscriptionChanged(selectedSubscription: azureResource.AzureResourceSubscription | undefined): void { + resourceGroupDropdown.values = []; + vscode.commands.executeCommand('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription).then(resourceGroups => { + resourceGroupDropdown.values = (resourceGroups).map(resourceGroup => resourceGroup.name).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase())); + }, err => { console.log(`Unexpected error fetching resource groups for subscription ${selectedSubscription?.name} (${selectedSubscription?.id}): ${err}`); }); +} + export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean { // Validate SQL Server password const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase()); @@ -441,7 +564,7 @@ export function getPasswordMismatchMessage(fieldName: string): string { export function setModelValues(inputComponents: InputComponents, model: Model): void { Object.keys(inputComponents).forEach(key => { let value; - const input = inputComponents[key]; + const input = inputComponents[key].component; if ('checked' in input) { // CheckBoxComponent value = input.checked ? 'true' : 'false'; } else if ('value' in input) { // InputBoxComponent or DropDownComponent @@ -455,6 +578,10 @@ export function setModelValues(inputComponents: InputComponents, model: Model): throw new Error(`Unknown input type with ID ${input.id}`); } + const inputValueTransformer = inputComponents[key].inputValueTransformer; + if (inputValueTransformer) { + value = inputValueTransformer(value || ''); + } model.setPropertyValue(key, value); }); }