diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json
index 7a7307b7ea..c34d737b9f 100644
--- a/extensions/azurecore/package.json
+++ b/extensions/azurecore/package.json
@@ -136,15 +136,15 @@
{
"id": "microsoft",
"icon": {
- "light": "./resources/light/microsoft_account_light.svg",
- "dark": "./resources/dark/microsoft_account_dark.svg"
+ "light": "./resources/users.svg",
+ "dark": "./resources/users.svg"
}
},
{
"id": "work_school",
"icon": {
- "light": "./resources/light/work_school_account_light.svg",
- "dark": "./resources/dark/work_school_account_dark.svg"
+ "light": "./resources/users.svg",
+ "dark": "./resources/users.svg"
}
}
],
@@ -174,6 +174,11 @@
"title": "%azure.resource.refresh.title%",
"icon": "$(refresh)"
},
+ {
+ "command": "azure.resource.selecttenants",
+ "title": "%azure.resource.selecttenants.title%",
+ "icon": "$(filter)"
+ },
{
"command": "azure.resource.selectsubscriptions",
"title": "%azure.resource.selectsubscriptions.title%",
@@ -237,6 +242,10 @@
"command": "azure.resource.selectsubscriptions",
"when": "false"
},
+ {
+ "command": "azure.resource.selecttenants",
+ "when": "false"
+ },
{
"command": "azure.resource.azureview.refresh",
"when": "false"
@@ -267,24 +276,44 @@
}
],
"view/item/context": [
+ {
+ "command": "azure.resource.selecttenants",
+ "when": "viewItem == azure.resource.itemType.multipleTenantAccount",
+ "group": "inline"
+ },
+ {
+ "command": "azure.resource.selecttenants",
+ "when": "viewItem == azure.resource.itemType.multipleTenantAccount",
+ "group": "azurecore"
+ },
{
"command": "azure.resource.selectsubscriptions",
- "when": "viewItem == azure.resource.itemType.account",
+ "when": "viewItem == azure.resource.itemType.singleTenantAccount",
"group": "inline"
},
{
"command": "azure.resource.selectsubscriptions",
- "when": "viewItem == azure.resource.itemType.account",
+ "when": "viewItem == azure.resource.itemType.singleTenantAccount",
+ "group": "azurecore"
+ },
+ {
+ "command": "azure.resource.selectsubscriptions",
+ "when": "viewItem == azure.resource.itemType.tenant",
+ "group": "inline"
+ },
+ {
+ "command": "azure.resource.selectsubscriptions",
+ "when": "viewItem == azure.resource.itemType.tenant",
"group": "azurecore"
},
{
"command": "azure.resource.azureview.refresh",
- "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer|synapseSqlPoolContainer|synapseWorkspaceContainer)$/",
+ "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:singleTenantAccount|multipleTenantAccount|subscription|tenant|databaseContainer|databaseServerContainer|synapseSqlPoolContainer|synapseWorkspaceContainer)$/",
"group": "inline"
},
{
"command": "azure.resource.azureview.refresh",
- "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer|synapseSqlPoolContainer|synapseWorkspaceContainer)$/",
+ "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:singleTenantAccount|multipleTenantAccount|subscription|tenant|databaseContainer|databaseServerContainer|synapseSqlPoolContainer|synapseWorkspaceContainer)$/",
"group": "azurecore"
},
{
@@ -299,7 +328,7 @@
},
{
"command": "azure.resource.startterminal",
- "when": "viewItem == azure.resource.itemType.account",
+ "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:singleTenantAccount|multipleTenantAccount)$/",
"group": "inline"
},
{
@@ -314,19 +343,29 @@
},
{
"command": "azure.resource.startterminal",
- "when": "viewItem == azure.resource.itemType.account",
+ "when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:singleTenantAccount|multipleTenantAccount)$/",
"group": "azurecore"
}
],
"connectionDialog/browseTree": [
{
"command": "azure.resource.selectsubscriptions",
- "when": "contextValue == azure.resource.itemType.account",
+ "when": "contextValue == azure.resource.itemType.tenant",
+ "group": "navigation"
+ },
+ {
+ "command": "azure.resource.selecttenants",
+ "when": "contextValue == azure.resource.itemType.multipleTenantAccount",
+ "group": "navigation"
+ },
+ {
+ "command": "azure.resource.selectsubscriptions",
+ "when": "contextValue == azure.resource.itemType.singleTenantAccount",
"group": "navigation"
},
{
"command": "azure.resource.connectiondialog.refresh",
- "when": "contextValue == azure.resource.itemType.account",
+ "when": "contextValue =~ /^azure\\.resource\\.itemType\\.(?:singleTenantAccount|multipleTenantAccount|tenant)$/",
"group": "navigation"
},
{
diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json
index a3e9b1949f..8c0c71ffa1 100644
--- a/extensions/azurecore/package.nls.json
+++ b/extensions/azurecore/package.nls.json
@@ -9,6 +9,7 @@
"azure.resource.refresh.title": "Refresh",
"azure.resource.signin.title": "Azure: Sign In",
"azure.resource.selectsubscriptions.title": "Select Subscriptions",
+ "azure.resource.selecttenants.title": "Select Tenants",
"azure.resource.startterminal.title": "Start Cloud Shell",
"azure.resource.connectsqlserver.title": "Connect",
"azure.resource.connectsqldb.title": "Add to Servers",
diff --git a/extensions/azurecore/resources/azureSynapseAnalytics.svg b/extensions/azurecore/resources/azureSynapseAnalytics.svg
new file mode 100644
index 0000000000..ec3d880261
--- /dev/null
+++ b/extensions/azurecore/resources/azureSynapseAnalytics.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/cosmosDb.svg b/extensions/azurecore/resources/cosmosDb.svg
new file mode 100644
index 0000000000..cd4106e79d
--- /dev/null
+++ b/extensions/azurecore/resources/cosmosDb.svg
@@ -0,0 +1,22 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/dataExplorerClusterDb.svg b/extensions/azurecore/resources/dataExplorerClusterDb.svg
new file mode 100644
index 0000000000..c598abebeb
--- /dev/null
+++ b/extensions/azurecore/resources/dataExplorerClusterDb.svg
@@ -0,0 +1,32 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/logAnalyticsWorkspaces.svg b/extensions/azurecore/resources/logAnalyticsWorkspaces.svg
new file mode 100644
index 0000000000..6e3efc8de5
--- /dev/null
+++ b/extensions/azurecore/resources/logAnalyticsWorkspaces.svg
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/mysqlDatabase.svg b/extensions/azurecore/resources/mysqlDatabase.svg
new file mode 100644
index 0000000000..dfae0e012d
--- /dev/null
+++ b/extensions/azurecore/resources/mysqlDatabase.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/sqlPools.svg b/extensions/azurecore/resources/sqlPools.svg
new file mode 100644
index 0000000000..59fd8a2cf8
--- /dev/null
+++ b/extensions/azurecore/resources/sqlPools.svg
@@ -0,0 +1,28 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/subscriptions.svg b/extensions/azurecore/resources/subscriptions.svg
new file mode 100644
index 0000000000..05c73987f2
--- /dev/null
+++ b/extensions/azurecore/resources/subscriptions.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/tenant.svg b/extensions/azurecore/resources/tenant.svg
new file mode 100644
index 0000000000..130e24b2c8
--- /dev/null
+++ b/extensions/azurecore/resources/tenant.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/extensions/azurecore/resources/users.svg b/extensions/azurecore/resources/users.svg
new file mode 100644
index 0000000000..fecd345aef
--- /dev/null
+++ b/extensions/azurecore/resources/users.svg
@@ -0,0 +1,16 @@
+
diff --git a/extensions/azurecore/src/account-provider/auths/azureAuth.ts b/extensions/azurecore/src/account-provider/auths/azureAuth.ts
index 2a75cd9406..e91a71111c 100644
--- a/extensions/azurecore/src/account-provider/auths/azureAuth.ts
+++ b/extensions/azurecore/src/account-provider/auths/azureAuth.ts
@@ -194,7 +194,7 @@ export abstract class AzureAuth implements vscode.Disposable {
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise {
let account: azdata.Account;
if (this._authLibrary === Constants.AuthLibrary.MSAL) {
- const tenants = await this.getTenantsMsal(token.token);
+ const tenants = await this.getTenantsMsal(token.token, tokenClaims);
account = this.createAccount(tokenClaims, token.key, tenants);
} else { // fallback to ADAL as default
const tenants = await this.getTenantsAdal({ ...token });
@@ -471,7 +471,7 @@ export abstract class AzureAuth implements vscode.Disposable {
return result;
}
- public async getTenantsMsal(token: string): Promise {
+ public async getTenantsMsal(token: string, tokenClaims: TokenClaims): Promise {
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
try {
Logger.verbose(`Fetching tenants with uri: ${tenantUri}`);
@@ -499,7 +499,7 @@ export abstract class AzureAuth implements vscode.Disposable {
return {
id: tenantInfo.tenantId,
displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
- userId: token,
+ userId: tokenClaims.oid,
tenantCategory: tenantInfo.tenantCategory
} as Tenant;
});
diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts
index e9167030c1..f6e8e6f266 100644
--- a/extensions/azurecore/src/azureResource/commands.ts
+++ b/extensions/azurecore/src/azureResource/commands.ts
@@ -12,12 +12,14 @@ import { AppContext } from '../appContext';
import { TreeNode } from './treeNode';
import { AzureResourceTreeProvider } from './tree/treeProvider';
import { AzureResourceAccountTreeNode } from './tree/accountTreeNode';
-import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from '../azureResource/interfaces';
+import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService, IAzureResourceTenantFilterService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from './constants';
import { AzureAccount, Tenant, azureResource } from 'azurecore';
-import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
+import { FlatTenantTreeNode } from './tree/flatTenantTreeNode';
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';
import { AzureResourceErrorMessageUtil, filterAccounts } from './utils';
+import { AzureResourceTenantTreeNode } from './tree/tenantTreeNode';
+import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider, authLibrary: string): void {
const trees = [azureViewTree, connectionDialogTree];
@@ -95,14 +97,21 @@ export function registerAzureResourceCommands(appContext: AppContext, azureViewT
});
// Resource Tree commands
-
+ // Supports selecting subscriptions from single tenant account tree nodes or tenant tree node.
vscode.commands.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => {
- if (!(node instanceof AzureResourceAccountTreeNode) && !(node instanceof FlatAccountTreeNode)) {
+ if (!(node instanceof AzureResourceAccountTreeNode) && !(node instanceof FlatAccountTreeNode)
+ && !(node instanceof AzureResourceTenantTreeNode) && !(node instanceof FlatTenantTreeNode)) {
return;
}
const account = node.account;
- if (!account) {
+
+ // Select first tenant from single tenant accounts
+ let tenant = node.account.properties.tenants[0];
+ if (node instanceof AzureResourceTenantTreeNode || node instanceof FlatTenantTreeNode) {
+ tenant = node.tenant;
+ }
+ if (!account || !tenant) {
return;
}
@@ -112,7 +121,8 @@ export function registerAzureResourceCommands(appContext: AppContext, azureViewT
let subscriptions: azureResource.AzureResourceSubscription[] = [];
if (subscriptions.length === 0) {
try {
- subscriptions = await subscriptionService.getSubscriptions(account);
+ let tenantIds = tenant ? [tenant.id] : account.properties.tenants.flatMap(t => t.id);
+ subscriptions = await subscriptionService.getSubscriptions(account, tenantIds);
} catch (error) {
account.isStale = true;
void vscode.window.showErrorMessage(AzureResourceErrorMessageUtil.getErrorMessage(error));
@@ -120,7 +130,7 @@ export function registerAzureResourceCommands(appContext: AppContext, azureViewT
}
}
- let selectedSubscriptions = await subscriptionFilterService.getSelectedSubscriptions(account);
+ let selectedSubscriptions = await subscriptionFilterService.getSelectedSubscriptions(account, tenant);
if (!selectedSubscriptions) {
selectedSubscriptions = [];
}
@@ -152,7 +162,57 @@ export function registerAzureResourceCommands(appContext: AppContext, azureViewT
}
selectedSubscriptions = selectedSubscriptionQuickPickItems.map((subscriptionItem) => subscriptionItem.subscription);
- await subscriptionFilterService.saveSelectedSubscriptions(account, selectedSubscriptions);
+ await subscriptionFilterService.saveSelectedSubscriptions(account, tenant, selectedSubscriptions);
+ }
+ });
+
+ vscode.commands.registerCommand('azure.resource.selecttenants', async (node?: TreeNode) => {
+ if (!(node instanceof AzureResourceAccountTreeNode) && !(node instanceof FlatAccountTreeNode)) {
+ return;
+ }
+
+ const account = node.account;
+ if (!account) {
+ return;
+ }
+
+ const tenantFilterService = appContext.getService(AzureResourceServiceNames.tenantFilterService);
+
+ let tenants = account.properties.tenants;
+
+ let selectedTenants = await tenantFilterService.getSelectedTenants(account);
+ if (!selectedTenants) {
+ selectedTenants = [];
+ }
+
+ const selectedTenantIds: string[] = [];
+ if (selectedTenants.length > 0) {
+ selectedTenantIds.push(...selectedTenants.map((tenant) => tenant.id));
+ } else {
+ // ALL tenants are selected by default
+ selectedTenantIds.push(...tenants.map((tenant) => tenant.id));
+ }
+
+ interface AzureResourceTenantQuickPickItem extends vscode.QuickPickItem {
+ tenant: Tenant;
+ }
+
+ const tenantQuickPickItems: AzureResourceTenantQuickPickItem[] = tenants.map(tenant => {
+ return {
+ label: tenant.displayName,
+ picked: selectedTenantIds.indexOf(tenant.id) !== -1,
+ tenant: tenant
+ };
+ }).sort((a, b) => a.label.localeCompare(b.label));
+
+ const selectedtenantQuickPickItems = await vscode.window.showQuickPick(tenantQuickPickItems, { canPickMany: true });
+ if (selectedtenantQuickPickItems && selectedtenantQuickPickItems.length > 0) {
+ for (const tree of trees) {
+ await tree.refresh(undefined, false);
+ }
+
+ selectedTenants = selectedtenantQuickPickItems.map((item) => item.tenant);
+ await tenantFilterService.saveSelectedTenants(account, selectedTenants);
}
});
@@ -167,7 +227,8 @@ export function registerAzureResourceCommands(appContext: AppContext, azureViewT
});
vscode.commands.registerCommand('azure.resource.connectiondialog.refresh', async (node?: TreeNode) => {
- return connectionDialogTree.refresh(node, true);
+ await connectionDialogTree.refresh(node, true); // clear cache first
+ return connectionDialogTree.refresh(node, false);
});
vscode.commands.registerCommand('azure.resource.signin', async (node?: TreeNode) => {
diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts
index e32fb39960..bbc52bf3e7 100644
--- a/extensions/azurecore/src/azureResource/constants.ts
+++ b/extensions/azurecore/src/azureResource/constants.ts
@@ -5,7 +5,10 @@
export enum AzureResourceItemType {
account = 'azure.resource.itemType.account',
+ singleTenantAccount = 'azure.resource.itemType.singleTenantAccount',
+ multipleTenantAccount = 'azure.resource.itemType.multipleTenantAccount',
subscription = 'azure.resource.itemType.subscription',
+ tenant = 'azure.resource.itemType.tenant',
databaseContainer = 'azure.resource.itemType.databaseContainer',
database = 'azure.resource.itemType.database',
databaseServerContainer = 'azure.resource.itemType.databaseServerContainer',
@@ -32,6 +35,7 @@ export enum AzureResourceServiceNames {
subscriptionService = 'AzureResourceSubscriptionService',
subscriptionFilterService = 'AzureResourceSubscriptionFilterService',
tenantService = 'AzureResourceTenantService',
+ tenantFilterService = 'AzureResourceTenantFilterService',
terminalService = 'AzureTerminalService',
}
diff --git a/extensions/azurecore/src/azureResource/interfaces.ts b/extensions/azurecore/src/azureResource/interfaces.ts
index 5b3c9a7786..5e4df246f0 100644
--- a/extensions/azurecore/src/azureResource/interfaces.ts
+++ b/extensions/azurecore/src/azureResource/interfaces.ts
@@ -135,9 +135,14 @@ export interface IAzureResourceSubscriptionService {
getSubscriptions(account: AzureAccount, tenantIds?: string[] | undefined): Promise;
}
+export interface IAzureResourceTenantFilterService {
+ getSelectedTenants(account: AzureAccount): Promise;
+ saveSelectedTenants(account: AzureAccount, selectedTenants: Tenant[]): Promise;
+}
+
export interface IAzureResourceSubscriptionFilterService {
- getSelectedSubscriptions(account: AzureAccount): Promise;
- saveSelectedSubscriptions(account: AzureAccount, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise;
+ getSelectedSubscriptions(account: AzureAccount, tenant: Tenant): Promise;
+ saveSelectedSubscriptions(account: AzureAccount, tenant: Tenant, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise;
}
export interface IAzureTerminalService {
diff --git a/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts
index 357a50679b..522edf3e02 100644
--- a/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
export class AzureMonitorTreeDataProvider extends ResourceTreeDataProviderBase {
private static readonly containerId = 'azure.resource.providers.AzureMonitorContainer';
- private static readonly containerLabel = localize('azure.resource.providers.AzureMonitorContainerLabel', "Log Analytics workspace");
+ private static readonly containerLabel = localize('azure.resource.providers.AzureMonitorContainerLabel', "Log Analytics workspaces");
public constructor(
databaseServerService: azureResource.IAzureResourceService,
@@ -27,12 +27,9 @@ export class AzureMonitorTreeDataProvider extends ResourceTreeDataProviderBase {
private static readonly CONTAINER_ID = 'azure.resource.providers.databaseServer.treeDataProvider.cosmosDbMongoContainer';
- private static readonly CONTAINER_LABEL = localize('azure.resource.providers.databaseServer.treeDataProvider.cosmosDbMongoContainerLabel', "CosmosDB for Mongo");
+ private static readonly CONTAINER_LABEL = localize('azure.resource.providers.databaseServer.treeDataProvider.cosmosDbMongoContainerLabel', "Azure CosmosDB for MongoDB");
public constructor(
databaseServerService: azureResource.IAzureResourceService,
@@ -28,12 +28,9 @@ export class CosmosDbMongoTreeDataProvider extends ResourceTreeDataProviderBase<
public getTreeItemForResource(databaseServer: AzureResourceMongoDatabaseServer, account: azdata.Account): azdata.TreeItem {
return {
- id: `${AzureResourcePrefixes.cosmosdb}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
+ id: `${AzureResourcePrefixes.cosmosdb}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
label: `${databaseServer.name} (CosmosDB Mongo API)`,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/cosmosdb_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/cosmosdb.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/cosmosDb.svg'),
collapsibleState: TreeItemCollapsibleState.None,
contextValue: AzureResourceItemType.cosmosDBMongoAccount,
payload: {
@@ -65,10 +62,7 @@ export class CosmosDbMongoTreeDataProvider extends ResourceTreeDataProviderBase<
return [{
id: CosmosDbMongoTreeDataProvider.CONTAINER_ID,
label: CosmosDbMongoTreeDataProvider.CONTAINER_LABEL,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/cosmosDb.svg'),
collapsibleState: TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServerContainer
}];
diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts
index 36d2e6ad4d..5b2b79ef68 100644
--- a/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts
@@ -17,7 +17,7 @@ import { AzureAccount, azureResource } from 'azurecore';
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 database");
+ private static readonly containerLabel = localize('azure.resource.providers.database.treeDataProvider.databaseContainerLabel', "SQL databases");
public constructor(
databaseService: azureResource.IAzureResourceService,
@@ -28,12 +28,9 @@ export class AzureResourceDatabaseTreeDataProvider extends ResourceTreeDataProvi
public getTreeItemForResource(database: azureResource.AzureResourceDatabase, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.databaseServer}${account.key.accountId}${database.serverFullName}.${AzureResourcePrefixes.database}${database.id ?? database.name}`,
+ id: `${AzureResourcePrefixes.database}${account.key.accountId}${database.tenant}${database.serverFullName}.${AzureResourcePrefixes.database}${database.id ?? database.name}`,
label: this.browseConnectionMode ? `${database.serverName}/${database.name} (${AzureResourceDatabaseTreeDataProvider.containerLabel}, ${database.subscription.name})` : `${database.name} (${database.serverName})`,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_database_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_database.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/sqlDatabase.svg'),
collapsibleState: this.browseConnectionMode ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.database,
payload: {
@@ -64,10 +61,7 @@ export class AzureResourceDatabaseTreeDataProvider extends ResourceTreeDataProvi
return [{
id: AzureResourceDatabaseTreeDataProvider.containerId,
label: AzureResourceDatabaseTreeDataProvider.containerLabel,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/sqlDatabase.svg'),
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseContainer
}];
diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts
index 9247b716aa..28c29a7d1f 100644
--- a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
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 server");
+ private static readonly containerLabel = localize('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainerLabel', "SQL servers");
public constructor(
databaseServerService: azureResource.IAzureResourceService,
@@ -27,12 +27,9 @@ export class AzureResourceDatabaseServerTreeDataProvider extends ResourceTreeDat
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.databaseServer}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
+ id: `${AzureResourcePrefixes.databaseServer}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
label: this.browseConnectionMode ? `${databaseServer.name} (${AzureResourceDatabaseServerTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/sqlServer.svg'),
collapsibleState: this.browseConnectionMode ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServer,
payload: {
@@ -63,10 +60,7 @@ export class AzureResourceDatabaseServerTreeDataProvider extends ResourceTreeDat
return [{
id: AzureResourceDatabaseServerTreeDataProvider.containerId,
label: AzureResourceDatabaseServerTreeDataProvider.containerLabel,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/sqlServer.svg'),
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServerContainer
}];
diff --git a/extensions/azurecore/src/azureResource/providers/kusto/kustoTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/kusto/kustoTreeDataProvider.ts
index 3dacda4036..ec963aaccc 100644
--- a/extensions/azurecore/src/azureResource/providers/kusto/kustoTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/kusto/kustoTreeDataProvider.ts
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
export class KustoTreeDataProvider extends ResourceTreeDataProviderBase {
private static readonly containerId = 'azure.resource.providers.KustoContainer';
- private static readonly containerLabel = localize('azure.resource.providers.KustoContainerLabel', "Azure Data Explorer Cluster");
+ private static readonly containerLabel = localize('azure.resource.providers.KustoContainerLabel', "Azure Data Explorer Clusters");
public constructor(
databaseServerService: azureResource.IAzureResourceService,
@@ -27,12 +27,9 @@ export class KustoTreeDataProvider extends ResourceTreeDataProviderBase {
private static readonly containerId = 'azure.resource.providers.postgresArcServer.treeDataProvider.postgresServerContainer';
// allow-any-unicode-next-line
- private static readonly containerLabel = localize('azure.resource.providers.postgresArcServer.treeDataProvider.postgresServerContainerLabel', "PostgreSQL Hyperscale – Azure Arc");
+ private static readonly containerLabel = localize('azure.resource.providers.postgresArcServer.treeDataProvider.postgresServerContainerLabel', "PostgreSQL servers – Azure Arc");
public constructor(
databaseServerService: azureResource.IAzureResourceService,
@@ -28,12 +28,9 @@ export class PostgresServerArcTreeDataProvider extends ResourceTreeDataProviderB
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.postgresServerArc}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
+ id: `${AzureResourcePrefixes.postgresServerArc}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
label: this.browseConnectionMode ? `${databaseServer.name} (${PostgresServerArcTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/azureArcPostgresServer.svg'),
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServer,
payload: {
@@ -67,10 +64,7 @@ export class PostgresServerArcTreeDataProvider extends ResourceTreeDataProviderB
return [{
id: PostgresServerArcTreeDataProvider.containerId,
label: PostgresServerArcTreeDataProvider.containerLabel,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/azureArcPostgresServer.svg'),
collapsibleState: TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServerContainer
}];
diff --git a/extensions/azurecore/src/azureResource/providers/postgresFlexibleServer/postgresFlexibleServerTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/postgresFlexibleServer/postgresFlexibleServerTreeDataProvider.ts
index e3cfb26f31..88f183d1b2 100644
--- a/extensions/azurecore/src/azureResource/providers/postgresFlexibleServer/postgresFlexibleServerTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/postgresFlexibleServer/postgresFlexibleServerTreeDataProvider.ts
@@ -27,12 +27,9 @@ export class PostgresFlexibleServerTreeDataProvider extends ResourceTreeDataProv
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.postgresFlexibleServer}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
+ id: `${AzureResourcePrefixes.postgresFlexibleServer}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
label: this.browseConnectionMode ? `${databaseServer.name} (${PostgresFlexibleServerTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/postgresServer.svg'),
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServer,
payload: {
@@ -66,10 +63,7 @@ export class PostgresFlexibleServerTreeDataProvider extends ResourceTreeDataProv
return [{
id: PostgresFlexibleServerTreeDataProvider.containerId,
label: PostgresFlexibleServerTreeDataProvider.containerLabel,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/postgresServer.svg'),
collapsibleState: TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServerContainer
}];
diff --git a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts
index ad0839eb55..baa4f77957 100644
--- a/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/postgresServer/postgresServerTreeDataProvider.ts
@@ -27,11 +27,11 @@ export class PostgresServerTreeDataProvider extends ResourceTreeDataProviderBase
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.postgresServer}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
+ id: `${AzureResourcePrefixes.postgresServer}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
label: this.browseConnectionMode ? `${databaseServer.name} (${PostgresServerTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name,
iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg')
+ dark: this._extensionContext.asAbsolutePath('resources/postgresServer.svg'),
+ light: this._extensionContext.asAbsolutePath('resources/postgresServer.svg')
},
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServer,
@@ -67,8 +67,8 @@ export class PostgresServerTreeDataProvider extends ResourceTreeDataProviderBase
id: PostgresServerTreeDataProvider.containerId,
label: PostgresServerTreeDataProvider.containerLabel,
iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
+ dark: this._extensionContext.asAbsolutePath('resources/postgresServer.svg'),
+ light: this._extensionContext.asAbsolutePath('resources/postgresServer.svg')
},
collapsibleState: TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServerContainer
diff --git a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts
index fdf94d12d1..2090f4579e 100644
--- a/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/sqlinstance/sqlInstanceTreeDataProvider.ts
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
export class SqlInstanceTreeDataProvider extends ResourceTreeDataProviderBase {
private static readonly containerId = 'azure.resource.providers.sqlInstanceContainer';
- private static readonly containerLabel = localize('azure.resource.providers.sqlInstanceContainerLabel', "Azure SQL DB managed instance");
+ private static readonly containerLabel = localize('azure.resource.providers.sqlInstanceContainerLabel', "SQL managed instances");
public constructor(
databaseServerService: azureResource.IAzureResourceService,
@@ -27,12 +27,9 @@ export class SqlInstanceTreeDataProvider extends ResourceTreeDataProviderBase {
private static readonly containerId = 'azure.resource.providers.sqlInstanceArcContainer';
// allow-any-unicode-next-line
- private static readonly containerLabel = localize('azure.resource.providers.sqlInstanceArcContainerLabel', "SQL managed instance – Azure Arc");
+ private static readonly containerLabel = localize('azure.resource.providers.sqlInstanceArcContainerLabel', "SQL managed instances - Azure Arc");
public constructor(
databaseServerService: azureResource.IAzureResourceService,
@@ -28,12 +28,9 @@ export class SqlInstanceArcTreeDataProvider extends ResourceTreeDataProviderBase
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.sqlInstanceArc}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
+ id: `${AzureResourcePrefixes.sqlInstanceArc}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
label: this.browseConnectionMode ? `${databaseServer.name} (${SqlInstanceArcTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_instance_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_instance.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/azureArcSqlManagedInstance.svg'),
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServer,
payload: {
@@ -64,10 +61,7 @@ export class SqlInstanceArcTreeDataProvider extends ResourceTreeDataProviderBase
return [{
id: SqlInstanceArcTreeDataProvider.containerId,
label: SqlInstanceArcTreeDataProvider.containerLabel,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/azureArcSqlManagedInstance.svg'),
collapsibleState: TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.databaseServerContainer
}];
diff --git a/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolTreeDataProvider.ts
index e3cc90330e..3cfb6164bf 100644
--- a/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/synapseSqlPool/synapseSqlPoolTreeDataProvider.ts
@@ -28,12 +28,9 @@ export class AzureResourceSynapseSqlPoolTreeDataProvider extends ResourceTreeDat
public getTreeItemForResource(synapse: azureResource.AzureResourceDatabase, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.synapseWorkspace}${account.key.accountId}${synapse.serverFullName}.${AzureResourcePrefixes.synapseSqlPool}${synapse.id ?? synapse.name}`,
+ id: `${AzureResourcePrefixes.synapseWorkspace}${account.key.accountId}${synapse.tenant}${synapse.serverFullName}.${AzureResourcePrefixes.synapseSqlPool}${synapse.id ?? synapse.name}`,
label: this.browseConnectionMode ? `${synapse.serverName}/${synapse.name} (${AzureResourceSynapseSqlPoolTreeDataProvider.containerLabel}, ${synapse.subscription.name})` : `${synapse.name} (${synapse.serverName})`,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_database_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_database.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/sqlPools.svg'),
collapsibleState: this.browseConnectionMode ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.synapseSqlPool,
payload: {
@@ -64,10 +61,7 @@ export class AzureResourceSynapseSqlPoolTreeDataProvider extends ResourceTreeDat
return [{
id: AzureResourceSynapseSqlPoolTreeDataProvider.containerId,
label: AzureResourceSynapseSqlPoolTreeDataProvider.containerLabel,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/sqlPools.svg'),
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.synapseSqlPoolContainer
}];
diff --git a/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceTreeDataProvider.ts
index 1b4671c007..f6b4443bca 100644
--- a/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceTreeDataProvider.ts
+++ b/extensions/azurecore/src/azureResource/providers/synapseWorkspace/synapseWorkspaceTreeDataProvider.ts
@@ -27,12 +27,9 @@ export class AzureResourceSynapseWorkspaceTreeDataProvider extends ResourceTreeD
public getTreeItemForResource(synapseWorkspace: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
return {
- id: `${AzureResourcePrefixes.synapseWorkspace}${account.key.accountId}${synapseWorkspace.id ?? synapseWorkspace.name}`,
+ id: `${AzureResourcePrefixes.synapseWorkspace}${account.key.accountId}${synapseWorkspace.tenant}${synapseWorkspace.id ?? synapseWorkspace.name}`,
label: this.browseConnectionMode ? `${synapseWorkspace.name} (${AzureResourceSynapseWorkspaceTreeDataProvider.containerLabel}, ${synapseWorkspace.subscription.name})` : synapseWorkspace.name,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/azureSynapseAnalytics.svg'),
collapsibleState: this.browseConnectionMode ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.synapseWorkspace,
payload: {
@@ -63,10 +60,7 @@ export class AzureResourceSynapseWorkspaceTreeDataProvider extends ResourceTreeD
return [{
id: AzureResourceSynapseWorkspaceTreeDataProvider.containerId,
label: AzureResourceSynapseWorkspaceTreeDataProvider.containerLabel,
- iconPath: {
- dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
- light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
- },
+ iconPath: this._extensionContext.asAbsolutePath('resources/azureSynapseAnalytics.svg'),
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: AzureResourceItemType.synapseWorkspaceContainer
}];
diff --git a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts
index eda2af3708..763f506fae 100644
--- a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts
+++ b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts
@@ -3,11 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { AzureAccount, azureResource } from 'azurecore';
+import { AzureAccount, Tenant, azureResource } from 'azurecore';
import { IAzureResourceSubscriptionFilterService, IAzureResourceCacheService } from '../interfaces';
interface AzureResourceSelectedSubscriptionsCache {
- selectedSubscriptions: { [accountId: string]: azureResource.AzureResourceSubscription[] };
+ selectedSubscriptions: { [accountTenantId: string]: azureResource.AzureResourceSubscription[] };
}
export class AzureResourceSubscriptionFilterService implements IAzureResourceSubscriptionFilterService {
@@ -19,36 +19,44 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub
this._cacheKey = this._cacheService.generateKey('selectedSubscriptions');
}
- public async getSelectedSubscriptions(account: AzureAccount): Promise {
+ public async getSelectedSubscriptions(account: AzureAccount, tenant: Tenant): Promise {
let selectedSubscriptions: azureResource.AzureResourceSubscription[] = [];
const cache = this._cacheService.get(this._cacheKey);
if (cache) {
- selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId];
+ selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId + '/' + tenant.id];
+ if (!selectedSubscriptions) {
+ let oldTenantCache = cache.selectedSubscriptions[account.key.accountId]?.filter(sub => sub.tenant === tenant.id);
+ if (oldTenantCache) {
+ await this.saveSelectedSubscriptions(account, tenant, oldTenantCache);
+ }
+ }
}
-
return selectedSubscriptions;
}
- public async saveSelectedSubscriptions(account: AzureAccount, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise {
- let selectedSubscriptionsCache: { [accountId: string]: azureResource.AzureResourceSubscription[] } = {};
+ public async saveSelectedSubscriptions(account: AzureAccount, tenant: Tenant, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise {
+ let selections: { [accountTenantId: string]: azureResource.AzureResourceSubscription[] } = {};
const cache = this._cacheService.get(this._cacheKey);
if (cache) {
- selectedSubscriptionsCache = cache.selectedSubscriptions;
+ selections = cache.selectedSubscriptions;
}
- if (!selectedSubscriptionsCache) {
- selectedSubscriptionsCache = {};
+ if (!selections) {
+ selections = {};
}
- selectedSubscriptionsCache[account.key.accountId] = selectedSubscriptions;
+ let accountTenantId = account.key.accountId;
+ if (tenant) {
+ accountTenantId += '/' + tenant.id;
+ }
- await this._cacheService.update(this._cacheKey, { selectedSubscriptions: selectedSubscriptionsCache });
+ selections[accountTenantId] = selectedSubscriptions;
+
+ await this._cacheService.update(this._cacheKey, { selectedSubscriptions: selections });
const filters: string[] = [];
- for (const accountId in selectedSubscriptionsCache) {
- filters.push(...selectedSubscriptionsCache[accountId].map((subscription) => `${accountId}/${subscription.id}/${subscription.name}`));
- }
+ filters.push(...selections[accountTenantId].map((subscription) => `${accountTenantId}/${subscription.id}`));
}
}
diff --git a/extensions/azurecore/src/azureResource/services/tenantFilterService.ts b/extensions/azurecore/src/azureResource/services/tenantFilterService.ts
new file mode 100644
index 0000000000..ff3ad0292e
--- /dev/null
+++ b/extensions/azurecore/src/azureResource/services/tenantFilterService.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.
+ *--------------------------------------------------------------------------------------------*/
+
+import { AzureAccount, Tenant } from 'azurecore';
+import { IAzureResourceTenantFilterService, IAzureResourceCacheService } from '../interfaces';
+
+interface AzureResourceSelectedTenantsCache {
+ selectedTenants: { [accountId: string]: Tenant[] };
+}
+
+export class AzureResourceTenantFilterService implements IAzureResourceTenantFilterService {
+ private _cacheKey: string;
+
+ public constructor(
+ private _cacheService: IAzureResourceCacheService
+ ) {
+ this._cacheKey = this._cacheService.generateKey('selectedTenants');
+ }
+
+ public async getSelectedTenants(account: AzureAccount): Promise {
+ let selectedTenants: Tenant[] = [];
+
+ const cache = this._cacheService.get(this._cacheKey);
+ if (cache) {
+ selectedTenants = cache.selectedTenants[account.key.accountId];
+ }
+
+ return selectedTenants;
+ }
+
+ public async saveSelectedTenants(account: AzureAccount, selectedTenants: Tenant[]): Promise {
+ let selectedTenantsCache: { [accountId: string]: Tenant[] } = {};
+
+ const cache = this._cacheService.get(this._cacheKey);
+ if (cache) {
+ selectedTenantsCache = cache.selectedTenants;
+ }
+
+ if (!selectedTenantsCache) {
+ selectedTenantsCache = {};
+ }
+
+ selectedTenantsCache[account.key.accountId] = selectedTenants;
+
+ await this._cacheService.update(this._cacheKey, { selectedTenants: selectedTenantsCache });
+
+ const filters: string[] = [];
+ for (const accountId in selectedTenantsCache) {
+ filters.push(...selectedTenantsCache[accountId].map((tenant) => `${accountId}/${tenant.id}/${tenant.displayName}`));
+ }
+ }
+}
diff --git a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts
index 272808473f..2835e8793b 100644
--- a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts
+++ b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts
@@ -11,16 +11,13 @@ const localize = nls.loadMessageBundle();
import { AppContext } from '../../appContext';
import { TreeNode } from '../treeNode';
-import { AzureSubscriptionError } from '../errors';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
-import { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode';
-import { AzureResourceMessageTreeNode } from '../messageTreeNode';
-import { AzureResourceErrorMessageUtil } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
-import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
-import { AzureAccount, azureResource } from 'azurecore';
-import { TenantIgnoredError } from '../../utils/TenantIgnoredError';
+import { AzureAccount, Tenant } from 'azurecore';
+import { AzureResourceTenantTreeNode } from './tenantTreeNode';
+import { AzureResourceMessageTreeNode } from '../messageTreeNode';
+import { IAzureResourceTenantFilterService } from '../interfaces';
export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(
@@ -29,92 +26,51 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
treeChangeHandler: IAzureResourceTreeChangeHandler
) {
super(appContext, treeChangeHandler, undefined);
+ this._tenantFilterService = this.appContext.getService(AzureResourceServiceNames.tenantFilterService);
- this._subscriptionService = this.appContext.getService(AzureResourceServiceNames.subscriptionService);
- this._subscriptionFilterService = this.appContext.getService(AzureResourceServiceNames.subscriptionFilterService);
-
+ if (this.account.properties.tenants.length === 1) {
+ this._singleTenantTreeNode = new AzureResourceTenantTreeNode(this.account, this.account.properties.tenants[0], this, this.appContext, this.treeChangeHandler);
+ }
this._id = `account_${this.account.key.accountId}`;
- this.setCacheKey(`${this._id}.subscriptions`);
+ this.setCacheKey(`${this._id}.tenants`);
this._label = this.generateLabel();
}
public async getChildren(): Promise {
- try {
- let subscriptions: azureResource.AzureResourceSubscription[] = [];
+ let tenants = this.account.properties.tenants;
+ this._totalTenantsCount = tenants.length;
- if (this._isClearingCache) {
- subscriptions = await this._subscriptionService.getSubscriptions(this.account);
- await this.updateCache(subscriptions);
- this._isClearingCache = false;
- } else {
- subscriptions = await this.getCachedSubscriptions();
- }
+ const selectedTenants = await this._tenantFilterService.getSelectedTenants(this.account);
+ const selectedTenantIds = (selectedTenants || []).map((Tenant) => Tenant.id);
- this._totalSubscriptionCount = subscriptions.length;
-
- const selectedSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account);
- const 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.noSubscriptionsLabel, this)];
- } else {
- const authLibrary = vscode.workspace.getConfiguration('azure').get('authenticationLibrary');
- if (authLibrary === 'ADAL') {
- // Filter out everything that we can't authenticate to.
- const hasTokenResults = await Promise.all(subscriptions.map(async s => {
- let token: azdata.accounts.AccountSecurityToken | undefined = undefined;
- let errMsg = '';
- try {
- token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant!, azdata.AzureResource.ResourceManagement);
- } catch (err) {
- if (!(err instanceof TenantIgnoredError)) {
- errMsg = AzureResourceErrorMessageUtil.getErrorMessage(err);
- }
- }
- if (!token) {
- if (errMsg !== '') {
- void vscode.window.showWarningMessage(localize('azure.unableToAccessSubscription', "Unable to access subscription {0} ({1}). Please [refresh the account](command:azure.resource.signin) to try again. {2}", s.name, s.id, errMsg));
- }
- return false;
- }
- return true;
- }));
- subscriptions = subscriptions.filter((_s, i) => hasTokenResults[i]);
- }
- let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
- return new AzureResourceSubscriptionTreeNode(this.account, subscription, subscription.tenant!, this.appContext, this.treeChangeHandler, this);
- }));
- return subTreeNodes.sort((a, b) => a.subscription.name.localeCompare(b.subscription.name));
- }
- } catch (error) {
- if (error instanceof AzureSubscriptionError) {
- void vscode.commands.executeCommand('azure.resource.signin');
- }
- return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
+ if (selectedTenantIds.length > 0) {
+ tenants = tenants.filter((tenant) => selectedTenantIds.indexOf(tenant.id) !== -1);
+ this._selectedTenantsCount = selectedTenantIds.length;
+ } else {
+ // ALL Tenants are listed by default
+ this._selectedTenantsCount = this._totalTenantsCount;
}
- }
- public async getCachedSubscriptions(): Promise {
- return this.getCache() ?? [];
+ this.refreshLabel();
+
+ if (this.totalTenantsCount === 1) {
+ return await this._singleTenantTreeNode?.getChildren() ?? [];
+ } else if (tenants.length === 0) {
+ return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.noTenantsLabel, this)];
+ } else {
+ let subTreeNodes = await Promise.all(tenants.map(async (tenant) => {
+ return new AzureResourceTenantTreeNode(this.account, tenant, this, this.appContext, this.treeChangeHandler);
+ }));
+ return subTreeNodes.sort((a, b) => a.tenant.displayName.localeCompare(b.tenant.displayName));
+ }
}
public getTreeItem(): vscode.TreeItem | Promise {
const item = new vscode.TreeItem(this._label, vscode.TreeItemCollapsibleState.Collapsed);
item.id = this._id;
- item.contextValue = AzureResourceItemType.account;
- item.iconPath = {
- dark: this.appContext.extensionContext.asAbsolutePath('resources/dark/account_inverse.svg'),
- light: this.appContext.extensionContext.asAbsolutePath('resources/light/account.svg')
- };
+ item.contextValue = this.account.properties.tenants.length > 1 ?
+ AzureResourceItemType.multipleTenantAccount : AzureResourceItemType.singleTenantAccount;
+ item.iconPath = this.appContext.extensionContext.asAbsolutePath('resources/users.svg');
return item;
}
@@ -137,12 +93,12 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
return this._id;
}
- public get totalSubscriptionCount(): number {
- return this._totalSubscriptionCount;
+ public get totalTenantsCount(): number {
+ return this._totalTenantsCount;
}
- public get selectedSubscriptionCount(): number {
- return this._selectedSubscriptionCount;
+ public get selectedTenantCount(): number {
+ return this._selectedTenantsCount;
}
protected refreshLabel(): void {
@@ -156,25 +112,23 @@ export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNode
private generateLabel(): string {
let label = this.account.displayInfo.displayName;
- if (this._totalSubscriptionCount !== 0) {
- label += ` (${this._selectedSubscriptionCount} / ${this._totalSubscriptionCount} subscriptions)`;
+ if (this._totalTenantsCount === 1 && this._singleTenantTreeNode) {
+ label += ` (${this._singleTenantTreeNode.selectedSubscriptionCount} / ${this._singleTenantTreeNode.totalSubscriptionCount} subscriptions)`;
+ } else if (this._totalTenantsCount > 0) {
+ label += ` (${this._selectedTenantsCount} / ${this._totalTenantsCount} tenants)`;
}
return label;
}
- private _subscriptionService: IAzureResourceSubscriptionService;
- private _subscriptionFilterService: IAzureResourceSubscriptionFilterService;
+ private _tenantFilterService: IAzureResourceTenantFilterService;
private _id: string;
private _label: string;
- private _totalSubscriptionCount = 0;
- private _selectedSubscriptionCount = 0;
+ private _totalTenantsCount = 0;
+ private _selectedTenantsCount = 0;
+ private _singleTenantTreeNode: AzureResourceTenantTreeNode | undefined;
- private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found.");
-
- sleep(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
+ private static readonly noTenantsLabel = localize('azure.resource.tree.accountTreeNode.noTenantsLabel', "No Tenants found.");
}
diff --git a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts
index 9312feab4f..d6c761a729 100644
--- a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts
+++ b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts
@@ -17,7 +17,6 @@ abstract class AzureResourceTreeNodeBase extends TreeNode {
parent: TreeNode | undefined
) {
super();
-
this.parent = parent;
}
}
@@ -29,7 +28,6 @@ export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTr
parent: TreeNode | undefined
) {
super(appContext, treeChangeHandler, parent);
-
this._cacheService = this.appContext.getService(AzureResourceServiceNames.cacheService);
}
diff --git a/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts b/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts
index 50fd13f7e4..e96f5caa05 100644
--- a/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts
+++ b/extensions/azurecore/src/azureResource/tree/connectionDialogTreeProvider.ts
@@ -15,9 +15,9 @@ import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceErrorMessageUtil, equals, filterAccounts } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
-import { FlatAccountTreeNode } from './flatAccountTreeNode';
import { Logger } from '../../utils/Logger';
import { AzureAccount } from 'azurecore';
+import { FlatAccountTreeNode } from './flatAccountTreeNode';
export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider, IAzureResourceTreeChangeHandler {
public isSystemInitialized: boolean = false;
@@ -45,7 +45,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider {
if (element) {
- return element.getChildren(true);
+ return element.getChildren();
}
if (!this.isSystemInitialized) {
@@ -63,6 +63,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider(AzureResourceServiceNames.subscriptionService);
- this._subscriptionFilterService = this.appContext.getService(AzureResourceServiceNames.subscriptionFilterService);
- this._resourceService = this.appContext.getService(AzureResourceServiceNames.resourceService);
+ this._tenantFilterService = this.appContext.getService(AzureResourceServiceNames.tenantFilterService);
this._id = `account_${this.account.key.accountId}`;
- this.setCacheKey(`${this._id}.dataresources`);
- this._label = account.displayInfo.displayName;
- this._loader = new FlatAccountTreeNodeLoader(appContext, this._resourceService, this._subscriptionService, this._subscriptionFilterService, this.account, this);
- this._loader.onNewResourcesAvailable(() => {
- this.treeChangeHandler.notifyNodeChanged(this);
- });
-
- this._loader.onLoadingStatusChanged(() => {
- this.treeChangeHandler.notifyNodeChanged(this);
- });
- }
-
- public async updateLabel(): Promise {
- const subscriptionInfo = await getSubscriptionInfo(this.account, this._subscriptionService, this._subscriptionFilterService);
- if (this._loader.isLoading) {
- this._label = localize('azure.resource.tree.accountTreeNode.titleLoading', "{0} - Loading...", this.account.displayInfo.displayName);
- } else if (subscriptionInfo.total !== 0) {
- this._label = localize({
- key: 'azure.resource.tree.accountTreeNode.title',
- comment: [
- '{0} is the display name of the azure account',
- '{1} is the number of selected subscriptions in this account',
- '{2} is the number of total subscriptions in this account'
- ]
- }, "{0} ({1}/{2} subscriptions)", this.account.displayInfo.displayName, subscriptionInfo.selected, subscriptionInfo.total);
- } else {
- this._label = this.account.displayInfo.displayName;
+ this.setCacheKey(`${this._id}.tenants`);
+ if (this.account.properties.tenants.length === 1) {
+ this._singleTenantTreeNode = new FlatTenantTreeNode(this.account, this.account.properties.tenants[0], this, this.appContext, this.treeChangeHandler);
}
+ this._label = this.generateLabel();
}
public async getChildren(): Promise {
+ let nodesResult: TreeNode[] = [];
+ let tenants = this.account.properties.tenants;
+ this._totalTenantsCount = tenants.length;
+
+ const selectedTenants = await this._tenantFilterService.getSelectedTenants(this.account);
+ const selectedTenantIds = (selectedTenants || []).map((Tenant) => Tenant.id);
+
+ if (selectedTenantIds.length > 0) {
+ tenants = tenants.filter((tenant) => selectedTenantIds.indexOf(tenant.id) !== -1);
+ this._selectedTenantsCount = selectedTenantIds.length;
+ } else {
+ // ALL Tenants are listed by default
+ this._selectedTenantsCount = this._totalTenantsCount;
+ }
+
+ this.refreshLabel();
+
+ if (this._totalTenantsCount === 1) {
+ if (this._isClearingCache) {
+ await this._singleTenantTreeNode?.getChildren();
+ } else {
+ nodesResult = await this._singleTenantTreeNode?.getChildren() ?? [];
+ }
+ } else if (tenants.length === 0) {
+ nodesResult = [AzureResourceMessageTreeNode.create(FlatAccountTreeNode.noTenantsLabel, this)];
+ } else {
+ if (!this._multiTenantTreeNodes) {
+ this._multiTenantTreeNodes = await Promise.all(tenants.map(async (tenant) => {
+ let node = new FlatTenantTreeNode(this.account, tenant, undefined, this.appContext, this.treeChangeHandler);
+ return node;
+ }));
+ }
+ nodesResult = this._multiTenantTreeNodes.sort((a, b) => a.tenant.displayName.localeCompare(b.tenant.displayName));
+ }
+
if (this._isClearingCache) {
- await this.updateLabel();
- this._loader.start().catch(err => console.error('Error loading Azure FlatAccountTreeNodes ', err));
+ this._multiTenantTreeNodes?.forEach(node => {
+ node.clearCache();
+ });
this._isClearingCache = false;
return [];
} else {
- return this._loader.nodes;
+ return nodesResult;
}
}
public getTreeItem(): vscode.TreeItem | Promise {
const item = new vscode.TreeItem(this._label, vscode.TreeItemCollapsibleState.Collapsed);
item.id = this._id;
- item.contextValue = AzureResourceItemType.account;
- item.iconPath = {
- dark: this.appContext.extensionContext.asAbsolutePath('resources/dark/account_inverse.svg'),
- light: this.appContext.extensionContext.asAbsolutePath('resources/light/account.svg')
- };
+ item.contextValue = this.account.properties.tenants.length > 1 ?
+ AzureResourceItemType.multipleTenantAccount : AzureResourceItemType.singleTenantAccount;
+ item.iconPath = this.appContext.extensionContext.asAbsolutePath('resources/users.svg');
return item;
}
@@ -107,130 +112,34 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
return this._id;
}
- private _subscriptionService: IAzureResourceSubscriptionService;
- private _subscriptionFilterService: IAzureResourceSubscriptionFilterService;
- private _resourceService: AzureResourceService;
- private _loader: FlatAccountTreeNodeLoader;
+ public refreshLabel(): void {
+ const newLabel = this.generateLabel();
+ if (this._label !== newLabel) {
+ this._label = newLabel;
+ this.treeChangeHandler.notifyNodeChanged(this);
+ }
+ }
+
+ private generateLabel(): string {
+ let label = this.account.displayInfo.displayName;
+
+ if (this._totalTenantsCount === 1 && this._singleTenantTreeNode) {
+ label += ` (${this._singleTenantTreeNode._selectedSubscriptionCount} / ${this._singleTenantTreeNode._totalSubscriptionCount} subscriptions)`;
+ } else if (this._totalTenantsCount > 0) {
+ label += ` (${this._selectedTenantsCount} / ${this._totalTenantsCount} tenants)`;
+ }
+
+ return label;
+ }
+
+ private _tenantFilterService: IAzureResourceTenantFilterService;
+
private _id: string;
private _label: string;
-}
-
-async function getSubscriptionInfo(account: AzureAccount, subscriptionService: IAzureResourceSubscriptionService, subscriptionFilterService: IAzureResourceSubscriptionFilterService): Promise<{
- subscriptions: azureResource.AzureResourceSubscription[],
- total: number,
- selected: number
-}> {
- let subscriptions = await subscriptionService.getSubscriptions(account);
- const total = subscriptions.length;
- let selected = total;
-
- const selectedSubscriptions = await subscriptionFilterService.getSelectedSubscriptions(account);
- const selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id);
- if (selectedSubscriptionIds.length > 0) {
- subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
- selected = selectedSubscriptionIds.length;
- }
- return {
- subscriptions,
- total,
- selected
- };
-}
-class FlatAccountTreeNodeLoader {
-
- private _isLoading: boolean = false;
- private _nodes: TreeNode[] = [];
- private readonly _onNewResourcesAvailable = new vscode.EventEmitter();
- public readonly onNewResourcesAvailable = this._onNewResourcesAvailable.event;
- private readonly _onLoadingStatusChanged = new vscode.EventEmitter();
- public readonly onLoadingStatusChanged = this._onLoadingStatusChanged.event;
-
- constructor(private readonly appContext: AppContext,
- private readonly _resourceService: AzureResourceService,
- private readonly _subscriptionService: IAzureResourceSubscriptionService,
- private readonly _subscriptionFilterService: IAzureResourceSubscriptionFilterService,
- private readonly _account: AzureAccount,
- private readonly _accountNode: TreeNode) {
- }
-
- public get isLoading(): boolean {
- return this._isLoading;
- }
-
- public get nodes(): TreeNode[] {
- return this._nodes;
- }
-
- public async start(): Promise {
- if (this._isLoading) {
- return;
- }
- this._isLoading = true;
- this._nodes = [];
- this._onLoadingStatusChanged.fire();
- let newNodesAvailable = false;
-
- // Throttle the refresh events to at most once per 500ms
- const refreshHandle = setInterval(() => {
- if (newNodesAvailable) {
- this._onNewResourcesAvailable.fire();
- newNodesAvailable = false;
- }
- if (!this.isLoading) {
- clearInterval(refreshHandle);
- }
- }, 500);
- try {
-
- // Authenticate to tenants to filter out subscriptions that are not accessible.
- let tenants = this._account.properties.tenants;
- // Filter out tenants that we can't authenticate to.
- tenants = tenants.filter(async tenant => {
- try {
- const token = await azdata.accounts.getAccountSecurityToken(this._account, tenant.id, azdata.AzureResource.ResourceManagement);
- return token !== undefined;
- } catch (e) {
- return false;
- }
- });
-
- let subscriptions: azureResource.AzureResourceSubscription[] = (await getSubscriptionInfo(this._account, this._subscriptionService, this._subscriptionFilterService)).subscriptions;
-
- if (subscriptions.length !== 0) {
- // Filter out subscriptions that don't belong to the tenants we filtered above.
- subscriptions = subscriptions.filter(async s => {
- const tenant = tenants.find(t => t.id === s.tenant);
- if (!tenant) {
- Logger.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
- return false;
- }
- return true;
- });
- }
-
- const resources = await this._resourceService.getAllChildren(this._account, subscriptions, true);
- if (resources?.length > 0) {
- this._nodes.push(...resources.map(dr => new AzureResourceResourceTreeNode(dr, this._accountNode, this.appContext)));
- this._nodes = this.nodes.sort((a, b) => {
- return a.getNodeInfo().label.localeCompare(b.getNodeInfo().label);
- });
- newNodesAvailable = true;
- }
- // Create "No Resources Found" message node if no resources found under azure account.
- if (this._nodes.length === 0) {
- this._nodes.push(AzureResourceMessageTreeNode.create(localize('azure.resource.flatAccountTreeNode.noResourcesLabel', "No Resources found."), this._accountNode))
- }
- } catch (error) {
- if (error instanceof AzureSubscriptionError) {
- void vscode.commands.executeCommand('azure.resource.signin');
- }
- // http status code 429 means "too many requests"
- // use a custom error message for azure resource graph api throttling error to make it more actionable for users.
- const errorMessage = error?.statusCode === 429 ? localize('azure.resource.throttleerror', "Requests from this account have been throttled. To retry, please select a smaller number of subscriptions.") : AzureResourceErrorMessageUtil.getErrorMessage(error);
- void vscode.window.showErrorMessage(localize('azure.resource.tree.loadresourceerror', "An error occurred while loading Azure resources: {0}", errorMessage));
- }
-
- this._isLoading = false;
- this._onLoadingStatusChanged.fire();
- }
+ private _totalTenantsCount = 0;
+ private _selectedTenantsCount = 0;
+ private _singleTenantTreeNode: FlatTenantTreeNode | undefined;
+ private _multiTenantTreeNodes: FlatTenantTreeNode[] | undefined;
+
+ private static readonly noTenantsLabel = localize('azure.resource.tree.accountTreeNode.noTenantsLabel', "No Tenants found.");
}
diff --git a/extensions/azurecore/src/azureResource/tree/flatTenantTreeNode.ts b/extensions/azurecore/src/azureResource/tree/flatTenantTreeNode.ts
new file mode 100644
index 0000000000..f221ea3bb1
--- /dev/null
+++ b/extensions/azurecore/src/azureResource/tree/flatTenantTreeNode.ts
@@ -0,0 +1,180 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as vscode from 'vscode';
+import * as azdata from 'azdata';
+
+import * as nls from 'vscode-nls';
+const localize = nls.loadMessageBundle();
+
+import { AppContext } from '../../appContext';
+import { TreeNode } from '../treeNode';
+import { AzureSubscriptionError } from '../errors';
+import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
+import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
+import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
+import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../interfaces';
+import { AzureAccount, Tenant, azureResource } from 'azurecore';
+import { AzureResourceService } from '../resourceService';
+import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
+import { AzureResourceErrorMessageUtil } from '../utils';
+import { AzureResourceMessageTreeNode } from '../messageTreeNode';
+import { FlatAccountTreeNode } from './flatAccountTreeNode';
+
+export class FlatTenantTreeNode extends AzureResourceContainerTreeNodeBase {
+ public constructor(
+ public readonly account: AzureAccount,
+ public readonly tenant: Tenant,
+ private readonly parentNode: FlatAccountTreeNode | undefined,
+ appContext: AppContext,
+ treeChangeHandler: IAzureResourceTreeChangeHandler,
+ ) {
+ super(appContext, treeChangeHandler, parentNode);
+
+ this._subscriptionService = this.appContext.getService(AzureResourceServiceNames.subscriptionService);
+ this._subscriptionFilterService = this.appContext.getService(AzureResourceServiceNames.subscriptionFilterService);
+ this._resourceService = this.appContext.getService(AzureResourceServiceNames.resourceService);
+
+ this._id = `account_${this.account.key.accountId}.tenant_${tenant.id}`;
+ this.setCacheKey(`${this._id}.dataresources`);
+ this._label = this.generateLabel();
+ }
+
+ public async getChildren(): Promise {
+ try {
+ let nodesResult: TreeNode[] = [];
+ let subscriptions: azureResource.AzureResourceSubscription[] = [];
+ if (this._isClearingCache) {
+ subscriptions = await this._subscriptionService.getSubscriptions(this.account, [this.tenant.id]);
+ await this.updateCache(subscriptions);
+ } else {
+ subscriptions = await this.getCachedSubscriptions();
+ }
+
+ this._totalSubscriptionCount = subscriptions.length;
+
+ const allSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account, this.tenant);
+ const selectedSubscriptions = allSubscriptions?.filter(subscription => subscription.tenant === this.tenant.id);
+ const 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 (this._isClearingCache) {
+ this._isClearingCache = false;
+ return [];
+ }
+
+ if (subscriptions.length === 0) {
+ nodesResult = [AzureResourceMessageTreeNode.create(FlatTenantTreeNode.noSubscriptionsLabel, this)];
+ } else {
+ let _nodes: AzureResourceResourceTreeNode[] = [];
+ const resources = await this._resourceService.getAllChildren(this.account, subscriptions, true);
+ if (resources?.length > 0) {
+ _nodes.push(...resources.map(dr => new AzureResourceResourceTreeNode(dr, this.parentNode ?? this, this.appContext)));
+ _nodes = _nodes.sort((a, b) => {
+ return a.getNodeInfo().label.localeCompare(b.getNodeInfo().label);
+ });
+ }
+ nodesResult = _nodes;
+ }
+
+ return nodesResult;
+ } catch (error) {
+ if (error instanceof AzureSubscriptionError) {
+ void vscode.commands.executeCommand('azure.resource.signin');
+ }
+ return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
+ }
+ }
+
+ public async getCachedSubscriptions(): Promise {
+ return this.getCache() ?? [];
+ }
+
+ public getTreeItem(): vscode.TreeItem | Promise {
+ const item = new vscode.TreeItem(this._label, vscode.TreeItemCollapsibleState.Collapsed);
+ item.id = this._id;
+ item.contextValue = AzureResourceItemType.tenant;
+ item.iconPath = this.appContext.extensionContext.asAbsolutePath('resources/tenant.svg');
+ return item;
+ }
+
+ protected refreshLabel(): void {
+ const newLabel = this.generateLabel();
+ if (this._label !== newLabel) {
+ this._label = newLabel;
+ this.treeChangeHandler.notifyNodeChanged(this);
+ }
+ }
+
+ private generateLabel(): string {
+ let label = this.tenant.displayName;
+
+ if (this._totalSubscriptionCount !== 0) {
+ label += ` (${this._selectedSubscriptionCount} / ${this._totalSubscriptionCount} subscriptions)`;
+ }
+
+ return label;
+ }
+
+ public getNodeInfo(): azdata.NodeInfo {
+ return {
+ label: this._label,
+ isLeaf: false,
+ errorMessage: undefined,
+ metadata: undefined,
+ nodePath: this.generateNodePath(),
+ parentNodePath: this.parent?.generateNodePath() ?? '',
+ nodeStatus: undefined,
+ nodeType: AzureResourceItemType.tenant,
+ nodeSubType: undefined,
+ iconType: AzureResourceItemType.tenant
+ };
+ }
+
+ public get nodePathValue(): string {
+ return this._id;
+ }
+
+ private _subscriptionService: IAzureResourceSubscriptionService;
+ private _subscriptionFilterService: IAzureResourceSubscriptionFilterService;
+ private _resourceService: AzureResourceService;
+
+ private _id: string;
+ private _label: string;
+ public _totalSubscriptionCount = 0;
+ public _selectedSubscriptionCount = 0;
+
+ private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found.");
+}
+
+export async function getSubscriptionInfo(account: AzureAccount, tenant: Tenant, subscriptionService: IAzureResourceSubscriptionService, subscriptionFilterService: IAzureResourceSubscriptionFilterService): Promise<{
+ subscriptions: azureResource.AzureResourceSubscription[],
+ total: number,
+ selected: number
+}> {
+ let subscriptions = await subscriptionService.getSubscriptions(account, [tenant.id]);
+ const total = subscriptions.length;
+ let selected = total;
+
+ const selectedSubscriptions = await subscriptionFilterService.getSelectedSubscriptions(account, tenant);
+ const selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id);
+ if (selectedSubscriptionIds.length > 0) {
+ subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
+ selected = selectedSubscriptionIds.length;
+ }
+ return {
+ subscriptions,
+ total,
+ selected
+ };
+}
diff --git a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts
index ecc7579d31..c3ed4e6624 100644
--- a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts
+++ b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts
@@ -18,20 +18,20 @@ import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';
import { AzureResourceService } from '../resourceService';
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
-import { AzureAccount, azureResource } from 'azurecore';
+import { AzureAccount, Tenant, azureResource } from 'azurecore';
export class AzureResourceSubscriptionTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(
public readonly account: AzureAccount,
public readonly subscription: azureResource.AzureResourceSubscription,
- public readonly tenantId: string,
+ public readonly tenant: Tenant,
appContext: AppContext,
treeChangeHandler: IAzureResourceTreeChangeHandler,
parent: TreeNode
) {
super(appContext, treeChangeHandler, parent);
- this._id = `account_${this.account.key.accountId}.subscription_${this.subscription.id}.tenant_${this.tenantId}`;
+ this._id = `account_${this.account.key.accountId}.tenant_${this.tenant.id}.subscription_${this.subscription.id}`;
this.setCacheKey(`${this._id}.resources`);
}
@@ -62,10 +62,7 @@ export class AzureResourceSubscriptionTreeNode extends AzureResourceContainerTre
public getTreeItem(): TreeItem | Promise {
const item = new TreeItem(this.subscription.name, TreeItemCollapsibleState.Collapsed);
item.contextValue = AzureResourceItemType.subscription;
- item.iconPath = {
- dark: this.appContext.extensionContext.asAbsolutePath('resources/dark/subscription_inverse.svg'),
- light: this.appContext.extensionContext.asAbsolutePath('resources/light/subscription.svg')
- };
+ item.iconPath = this.appContext.extensionContext.asAbsolutePath('resources/subscriptions.svg');
return item;
}
diff --git a/extensions/azurecore/src/azureResource/tree/tenantTreeNode.ts b/extensions/azurecore/src/azureResource/tree/tenantTreeNode.ts
new file mode 100644
index 0000000000..976e3c45b9
--- /dev/null
+++ b/extensions/azurecore/src/azureResource/tree/tenantTreeNode.ts
@@ -0,0 +1,156 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as vscode from 'vscode';
+import * as azdata from 'azdata';
+
+import * as nls from 'vscode-nls';
+const localize = nls.loadMessageBundle();
+
+import { AppContext } from '../../appContext';
+import { TreeNode } from '../treeNode';
+import { AzureSubscriptionError } from '../errors';
+import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
+import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
+import { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode';
+import { AzureResourceMessageTreeNode } from '../messageTreeNode';
+import { AzureResourceErrorMessageUtil } from '../utils';
+import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
+import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
+import { AzureAccount, Tenant, azureResource } from 'azurecore';
+import { AzureResourceAccountTreeNode } from './accountTreeNode';
+
+export class AzureResourceTenantTreeNode extends AzureResourceContainerTreeNodeBase {
+ public constructor(
+ public readonly account: AzureAccount,
+ public readonly tenant: Tenant,
+ parentNode: AzureResourceAccountTreeNode,
+ appContext: AppContext,
+ treeChangeHandler: IAzureResourceTreeChangeHandler
+ ) {
+ super(appContext, treeChangeHandler, parentNode);
+
+ this._subscriptionService = this.appContext.getService(AzureResourceServiceNames.subscriptionService);
+ this._subscriptionFilterService = this.appContext.getService(AzureResourceServiceNames.subscriptionFilterService);
+
+ this._id = `account_${this.account.key.accountId}.tenant_${tenant.id}`;
+ this.setCacheKey(`${this._id}.subscriptions`);
+ this._label = this.generateLabel();
+ }
+
+ public async getChildren(): Promise {
+ try {
+ let subscriptions: azureResource.AzureResourceSubscription[] = [];
+
+ if (this._isClearingCache) {
+ subscriptions = await this._subscriptionService.getSubscriptions(this.account, [this.tenant.id]);
+ await this.updateCache(subscriptions);
+ this._isClearingCache = false;
+ } else {
+ subscriptions = await this.getCachedSubscriptions();
+ }
+
+ this._totalSubscriptionCount = subscriptions.length;
+
+ const allSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account, this.tenant);
+ const selectedSubscriptions = allSubscriptions?.filter(subscription => subscription.tenant === this.tenant.id);
+ const 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(AzureResourceTenantTreeNode.noSubscriptionsLabel, this)];
+ } else {
+ let subTreeNodes = await Promise.all(subscriptions.map(async (subscription) => {
+ return new AzureResourceSubscriptionTreeNode(this.account, subscription, this.tenant, this.appContext, this.treeChangeHandler, this);
+ }));
+ return subTreeNodes.sort((a, b) => a.subscription.name.localeCompare(b.subscription.name));
+ }
+ } catch (error) {
+ if (error instanceof AzureSubscriptionError) {
+ void vscode.commands.executeCommand('azure.resource.signin');
+ }
+ return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
+ }
+ }
+
+ public async getCachedSubscriptions(): Promise {
+ return this.getCache() ?? [];
+ }
+
+ public getTreeItem(): vscode.TreeItem | Promise {
+ const item = new vscode.TreeItem(this._label, vscode.TreeItemCollapsibleState.Collapsed);
+ item.id = this._id;
+ item.contextValue = AzureResourceItemType.tenant;
+ item.iconPath = this.appContext.extensionContext.asAbsolutePath('resources/tenant.svg');
+ return item;
+ }
+
+ public getNodeInfo(): azdata.NodeInfo {
+ return {
+ label: this._label,
+ isLeaf: false,
+ errorMessage: undefined,
+ metadata: undefined,
+ nodePath: this.generateNodePath(),
+ parentNodePath: this.parent?.generateNodePath() ?? '',
+ nodeStatus: undefined,
+ nodeType: AzureResourceItemType.tenant,
+ nodeSubType: undefined,
+ iconType: AzureResourceItemType.tenant
+ };
+ }
+
+ 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);
+ }
+ }
+
+ private generateLabel(): string {
+ let label = this.tenant.displayName;
+
+ if (this._totalSubscriptionCount !== 0) {
+ label += ` (${this._selectedSubscriptionCount} / ${this._totalSubscriptionCount} subscriptions)`;
+ }
+
+ return label;
+ }
+
+ private _subscriptionService: IAzureResourceSubscriptionService;
+ private _subscriptionFilterService: IAzureResourceSubscriptionFilterService;
+
+ private _id: string;
+ private _label: string;
+ private _totalSubscriptionCount = 0;
+ private _selectedSubscriptionCount = 0;
+
+ private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found.");
+
+ sleep(ms: number) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+}
diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts
index a9c51e469d..4ce5281213 100644
--- a/extensions/azurecore/src/azureResource/tree/treeProvider.ts
+++ b/extensions/azurecore/src/azureResource/tree/treeProvider.ts
@@ -44,7 +44,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider {
if (element) {
- return element.getChildren(true);
+ return element.getChildren();
}
if (!this.isSystemInitialized) {
@@ -55,11 +55,15 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider 0) {
- this.accounts = filterAccounts(this.accounts, this.authLibrary);
- return this.accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this));
+ if (this.accounts) {
+ if (this.accounts.length === 0) {
+ return [new AzureResourceAccountNotSignedInTreeNode()];
+ } else {
+ this.accounts = filterAccounts(this.accounts, this.authLibrary);
+ return this.accounts.map((account) => new AzureResourceAccountTreeNode(account, this.appContext, this));
+ }
} else {
- return [new AzureResourceAccountNotSignedInTreeNode()];
+ return [AzureResourceMessageTreeNode.create(localize('azure.resource.tree.treeProvider.loadingLabel', "Loading ..."), undefined)];
}
} catch (error) {
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), undefined)];
@@ -68,7 +72,10 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider {
try {
- this.accounts = filterAccounts(await azdata.accounts.getAllAccounts(), this.authLibrary);
+ let accounts = await azdata.accounts.getAllAccounts();
+ if (accounts) {
+ this.accounts = filterAccounts(accounts, this.authLibrary);
+ }
// System has been initialized
this.setSystemInitialized();
this._onDidChangeTreeData.fire(undefined);
diff --git a/extensions/azurecore/src/azureResource/treeNode.ts b/extensions/azurecore/src/azureResource/treeNode.ts
index 1ed289a924..7d57ad6405 100644
--- a/extensions/azurecore/src/azureResource/treeNode.ts
+++ b/extensions/azurecore/src/azureResource/treeNode.ts
@@ -39,7 +39,7 @@ export abstract class TreeNode {
}
// TODO support filtering by already expanded / not yet expanded
- let children = await node.getChildren(false);
+ let children = await node.getChildren();
if (children) {
for (let child of children) {
if (filter && filter(child)) {
@@ -55,7 +55,7 @@ export abstract class TreeNode {
public parent: TreeNode | undefined = undefined;
- public abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise;
+ public abstract getChildren(): TreeNode[] | Promise;
public abstract getTreeItem(): vscode.TreeItem | Promise;
public abstract getNodeInfo(): azdata.NodeInfo;
diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts
index 6f0663b016..2712650d6b 100644
--- a/extensions/azurecore/src/azureResource/utils.ts
+++ b/extensions/azurecore/src/azureResource/utils.ts
@@ -14,7 +14,7 @@ import { EOL } from 'os';
import { AppContext } from '../appContext';
import { invalidAzureAccount, invalidTenant, unableToFetchTokenError } from '../localizedConstants';
import { AzureResourceServiceNames } from './constants';
-import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService } from './interfaces';
+import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService, IAzureResourceTenantFilterService } from './interfaces';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
import providerSettings from '../account-provider/providerSettings';
@@ -418,8 +418,12 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?:
}
const subscriptionFilterService = appContext.getService(AzureResourceServiceNames.subscriptionFilterService);
+ const tenantFilterService = appContext.getService(AzureResourceServiceNames.tenantFilterService);
try {
- result.subscriptions.push(...await subscriptionFilterService.getSelectedSubscriptions(account));
+ const tenants = await tenantFilterService.getSelectedTenants(account);
+ for (const tenant of tenants) {
+ result.subscriptions.push(...await subscriptionFilterService.getSelectedSubscriptions(account, tenant));
+ }
} catch (err) {
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.queryError', "Error fetching subscriptions for account {0} : {1}",
account.displayInfo.displayName,
diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts
index 8a5103bee5..40460fff2b 100644
--- a/extensions/azurecore/src/extension.ts
+++ b/extensions/azurecore/src/extension.ts
@@ -12,7 +12,7 @@ import * as os from 'os';
import { AppContext } from './appContext';
import { AzureAccountProviderService } from './account-provider/azureAccountProviderService';
import { AzureResourceService } from './azureResource/resourceService';
-import { IAzureResourceCacheService, IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from './azureResource/interfaces';
+import { IAzureResourceCacheService, IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService, IAzureResourceTenantFilterService } from './azureResource/interfaces';
import { AzureResourceServiceNames } from './azureResource/constants';
import { AzureResourceSubscriptionService } from './azureResource/services/subscriptionService';
import { AzureResourceSubscriptionFilterService } from './azureResource/services/subscriptionFilterService';
@@ -29,11 +29,10 @@ import { AzureResourceGroupService } from './azureResource/providers/resourceGro
import { Logger } from './utils/Logger';
import { ConnectionDialogTreeProvider } from './azureResource/tree/connectionDialogTreeProvider';
import { AzureDataGridProvider } from './azureDataGridProvider';
-// import { AzureResourceUniversalService } from './azureResource/providers/universal/universalService';
import { AzureResourceUniversalService } from './azureResource/providers/universal/universalService';
import { AzureResourceUniversalTreeDataProvider } from './azureResource/providers/universal/universalTreeDataProvider';
import { AzureResourceUniversalResourceProvider } from './azureResource/providers/universal/universalProvider';
-// import { AzureResourceUniversalTreeDataProvider } from './azureResource/providers/universal/universalTreeDataProvider';
+import { AzureResourceTenantFilterService } from './azureResource/services/tenantFilterService';
let extensionContext: vscode.ExtensionContext;
@@ -275,6 +274,7 @@ function registerAzureServices(appContext: AppContext): void {
appContext.registerService(AzureResourceServiceNames.cacheService, new AzureResourceCacheService(extensionContext));
appContext.registerService(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService());
appContext.registerService(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(extensionContext)));
+ appContext.registerService(AzureResourceServiceNames.tenantFilterService, new AzureResourceTenantFilterService(new AzureResourceCacheService(extensionContext)));
appContext.registerService(AzureResourceServiceNames.terminalService, new AzureTerminalService(extensionContext));
}
diff --git a/extensions/azurecore/src/test/azureResource/providers/database/databaseTreeDataProvider.test.ts b/extensions/azurecore/src/test/azureResource/providers/database/databaseTreeDataProvider.test.ts
index a67ce7689e..06980ca57c 100644
--- a/extensions/azurecore/src/test/azureResource/providers/database/databaseTreeDataProvider.test.ts
+++ b/extensions/azurecore/src/test/azureResource/providers/database/databaseTreeDataProvider.test.ts
@@ -77,6 +77,7 @@ const mockDatabases: azureResource.AzureResourceDatabase[] = [
name: 'mock database 1',
id: 'mock-id-1',
provider: DATABASE_PROVIDER_ID,
+ tenant: 'mockTenantId',
serverName: 'mock database server 1',
serverFullName: 'mock database server full name 1',
loginName: 'mock login',
@@ -90,6 +91,7 @@ const mockDatabases: azureResource.AzureResourceDatabase[] = [
name: 'mock database 2',
id: 'mock-id-2',
provider: DATABASE_PROVIDER_ID,
+ tenant: 'mockTenantId',
serverName: 'mock database server 2',
serverFullName: 'mock database server full name 2',
loginName: 'mock login',
@@ -140,7 +142,7 @@ describe('AzureResourceDatabaseTreeDataProvider.getChildren', function (): void
const child = children[0];
should(child.id).equal('azure.resource.providers.database.treeDataProvider.databaseContainer');
- should(child.label).equal('SQL database');
+ should(child.label).equal('SQL databases');
should(child.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
should(child.contextValue).equal('azure.resource.itemType.databaseContainer');
});
@@ -160,7 +162,7 @@ describe('AzureResourceDatabaseTreeDataProvider.getChildren', function (): void
should(child.account).equal(mockAccount);
should(child.subscription).equal(mockSubscription);
should(child.tenantId).equal(mockTenantId);
- should(child.treeItem.id).equal(`databaseServer_${mockAccount.key.accountId}${database.serverFullName}.database_${database.id}`);
+ should(child.treeItem.id).equal(`database_${mockAccount.key.accountId}${database.tenant}${database.serverFullName}.database_${database.id}`);
should(child.treeItem.label).equal(`${database.name} (${database.serverName})`);
should(child.treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
should(child.treeItem.contextValue).equal(AzureResourceItemType.database);
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 b87abb565d..f99106a1c4 100644
--- a/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts
+++ b/extensions/azurecore/src/test/azureResource/providers/databaseServer/databaseServerTreeDataProvider.test.ts
@@ -60,7 +60,7 @@ const mockResourceRootNode: azureResource.IAzureResourceNode = {
treeItem: {
id: 'mock_resource_root_node',
label: 'mock resource root node',
- iconPath: undefined,
+ iconPath: '',
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: 'mock_resource_root_node'
}
@@ -76,6 +76,7 @@ const mockDatabaseServers: azureResource.AzureResourceDatabaseServer[] = [
name: 'mock database server 1',
id: 'mock-id-1',
provider: DATABASE_SERVER_PROVIDER_ID,
+ tenant: 'mockTenantId',
fullName: 'mock database server full name 1',
loginName: 'mock login',
defaultDatabaseName: 'master',
@@ -89,6 +90,7 @@ const mockDatabaseServers: azureResource.AzureResourceDatabaseServer[] = [
name: 'mock database server 2',
id: 'mock-id-2',
provider: DATABASE_SERVER_PROVIDER_ID,
+ tenant: 'mockTenantId',
fullName: 'mock database server full name 2',
loginName: 'mock login',
defaultDatabaseName: 'master',
@@ -139,7 +141,7 @@ describe('AzureResourceDatabaseServerTreeDataProvider.getChildren', function ():
const child = children[0];
should(child.id).equal('azure.resource.providers.databaseServer.treeDataProvider.databaseServerContainer');
- should(child.label).equal('SQL server');
+ should(child.label).equal('SQL servers');
should(child.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
should(child.contextValue).equal('azure.resource.itemType.databaseServerContainer');
});
@@ -159,7 +161,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_${mockAccount.key.accountId}${databaseServer.id}`);
+ should(child.treeItem.id).equal(`databaseServer_${mockAccount.key.accountId}${databaseServer.tenant}${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/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts
index 1b49556584..ccf1c17fb1 100644
--- a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts
+++ b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts
@@ -15,6 +15,7 @@ import {
IAzureResourceCacheService,
IAzureResourceSubscriptionService,
IAzureResourceSubscriptionFilterService,
+ IAzureResourceTenantFilterService,
} from '../../../azureResource/interfaces';
import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler';
import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode';
@@ -24,15 +25,15 @@ import { AzureResourceMessageTreeNode } from '../../../azureResource/messageTree
import { generateGuid } from '../../../azureResource/utils';
import { AzureAccount, azureResource } from 'azurecore';
import allSettings from '../../../account-provider/providerSettings';
+import { AzureResourceTenantTreeNode } from '../../../azureResource/tree/tenantTreeNode';
// Mock services
let mockExtensionContext: TypeMoq.IMock;
let mockCacheService: TypeMoq.IMock;
-let mockSubscriptionServiceADAL: TypeMoq.IMock;
-let mockSubscriptionServiceMSAL: TypeMoq.IMock;
+let mockSubscriptionService: TypeMoq.IMock;
let mockSubscriptionFilterService: TypeMoq.IMock;
-let mockAppContextADAL: AppContext;
-let mockAppContextMSAL: AppContext;
+let mockTenantFilterService: TypeMoq.IMock;
+let mockAppContext: AppContext;
let mockTreeChangeHandler: TypeMoq.IMock;
// Mock test data
@@ -41,6 +42,13 @@ const mockTenant = {
id: mockTenantId,
displayName: 'Mock Tenant'
};
+const mockTenantAlternative = {
+ id: 'mock_tenant_id_alt',
+ displayName: 'Mock Tenant Alternative'
+};
+
+const mockTenants = [mockTenant, mockTenantAlternative];
+
const mockAccount: AzureAccount = {
key: {
accountId: '97915f6d-84fa-4926-b60c-38db64327ad7',
@@ -54,9 +62,7 @@ const mockAccount: AzureAccount = {
email: '97915f6d-84fa-4926-b60c-38db64327ad7'
},
properties: {
- tenants: [
- mockTenant
- ],
+ tenants: mockTenants,
owningTenant: mockTenant,
providerSettings: {
settings: allSettings[0].metadata.settings,
@@ -68,6 +74,8 @@ const mockAccount: AzureAccount = {
isStale: false
};
+const mockFilteredTenants = [mockTenant];
+
const mock_subscription_id_1 = 'mock_subscription_1';
const mockSubscription1: azureResource.AzureResourceSubscription = {
id: mock_subscription_id_1,
@@ -97,25 +105,20 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType();
mockCacheService = TypeMoq.Mock.ofType();
- mockSubscriptionServiceADAL = TypeMoq.Mock.ofType();
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType();
- mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
+ mockSubscriptionService = TypeMoq.Mock.ofType();
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
mockSubscriptionFilterService = TypeMoq.Mock.ofType();
+ mockTenantFilterService = TypeMoq.Mock.ofType();
mockTreeChangeHandler = TypeMoq.Mock.ofType();
mockSubscriptionCache = [];
- mockAppContextADAL = new AppContext(mockExtensionContext.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
-
- mockAppContextMSAL = new AppContext(mockExtensionContext.object);
- mockAppContextMSAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
- mockAppContextMSAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
- mockAppContextMSAL.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
+ mockAppContext = new AppContext(mockExtensionContext.object);
+ mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.tenantFilterService, mockTenantFilterService.object);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
@@ -129,8 +132,8 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
sinon.restore();
});
- it('Should be correct when created for ADAL.', async function (): Promise {
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
+ it('Should be correct when created.', async function (): Promise {
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
@@ -139,7 +142,7 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.id).equal(accountTreeNodeId);
should(treeItem.label).equal(mockAccount.displayInfo.displayName);
- should(treeItem.contextValue).equal(AzureResourceItemType.account);
+ should(treeItem.contextValue).equal(AzureResourceItemType.multipleTenantAccount);
should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
const nodeInfo = accountTreeNode.getNodeInfo();
@@ -149,146 +152,126 @@ describe('AzureResourceAccountTreeNode.info', function (): void {
should(nodeInfo.iconType).equal(AzureResourceItemType.account);
});
- it('Should be correct when created for MSAL.', async function (): Promise {
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
-
- const accountTreeNodeId = `account_${mockAccount.key.accountId}`;
-
- should(accountTreeNode.nodePathValue).equal(accountTreeNodeId);
-
- const treeItem = await accountTreeNode.getTreeItem();
- should(treeItem.id).equal(accountTreeNodeId);
- should(treeItem.label).equal(mockAccount.displayInfo.displayName);
- should(treeItem.contextValue).equal(AzureResourceItemType.account);
- should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed);
-
- const nodeInfo = accountTreeNode.getNodeInfo();
- should(nodeInfo.label).equal(mockAccount.displayInfo.displayName);
- 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 for ADAL.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
+ it('Should be correct when there are tenants available.', async function (): Promise {
+ mockTenantFilterService.setup((o) => o.getSelectedTenants(mockAccount)).returns(() => Promise.resolve(mockFilteredTenants));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
- const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
+ const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredTenants.length} / ${mockTenants.length} tenants)`;
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
- const subscriptionNodes = await accountTreeNode.getChildren();
+ const tenantNodes = await accountTreeNode.getChildren();
+
+ should(tenantNodes).Array();
+ should(tenantNodes.length).equal(mockFilteredTenants.length);
+
+ 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 listed.', async function (): Promise {
+ mockTenantFilterService.setup((o) => o.getSelectedTenants(mockAccount)).returns(() => Promise.resolve(mockFilteredTenants));
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
+ mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount, mockTenant)).returns(() => Promise.resolve([]));
+ sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
+
+ const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredTenants.length} / ${mockTenants.length} tenants)`;
+ const tenantTreeNodeLabel = `${mockTenant.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
+
+ // Validate account tree node
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
+ const tenantNodes = await accountTreeNode.getChildren();
+
+ should(tenantNodes).Array();
+ should(tenantNodes.length).equal(mockFilteredTenants.length);
+
+ const treeItem = await accountTreeNode.getTreeItem();
+ should(treeItem.label).equal(accountTreeNodeLabel);
+
+ const nodeInfo = accountTreeNode.getNodeInfo();
+ should(nodeInfo.label).equal(accountTreeNodeLabel);
+
+ // Validate tenant tree node
+ const tenantTreeNode = tenantNodes[0];
+ const subscriptions = await tenantTreeNode.getChildren();
+
+ should(subscriptions).Array();
+ should(subscriptions.length).equal(mockSubscriptions.length);
+
+ const subTreeItem = await tenantTreeNode.getTreeItem();
+ should(subTreeItem.label).equal(tenantTreeNodeLabel);
+
+ const subNodeInfo = tenantTreeNode.getNodeInfo();
+ should(subNodeInfo.label).equal(tenantTreeNodeLabel);
+ });
+
+ it('Should only show subscriptions with valid tokens.', async function (): Promise {
+ mockTenantFilterService.setup((o) => o.getSelectedTenants(mockAccount)).returns(() => Promise.resolve(mockTenants));
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
+ mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount, mockTenant)).returns(() => Promise.resolve(mockSubscriptions));
+ sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
+
+ const tenantTreeNodeLabel = `${mockTenant.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
+ const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockTenants.length} / ${mockTenants.length} tenants)`;
+
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
+ const tenantTreeNode = (await accountTreeNode.getChildren())[0];
+ const subscriptionNodes = await tenantTreeNode.getChildren();
+
+ // Validate account tree node
+ const treeItem = await accountTreeNode.getTreeItem();
+ should(treeItem.label).equal(accountTreeNodeLabel);
+
+ const nodeInfo = accountTreeNode.getNodeInfo();
+ should(nodeInfo.label).equal(accountTreeNodeLabel);
+
+ // Validate tenant tree node
+ const tenantTreeItem = await tenantTreeNode.getTreeItem();
+ should(tenantTreeItem.label).equal(tenantTreeNodeLabel);
+
+ const tenantNodeInfo = tenantTreeNode.getNodeInfo();
+ should(tenantNodeInfo.label).equal(tenantTreeNodeLabel);
should(subscriptionNodes).Array();
should(subscriptionNodes.length).equal(mockSubscriptions.length);
- 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 listed for MSAL.', async function (): Promise {
- mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
+ it('Should be correct when there are subscriptions filtered.', async function (): Promise {
+ mockTenantFilterService.setup((o) => o.getSelectedTenants(mockAccount)).returns(() => Promise.resolve(mockFilteredTenants));
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
+ mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount, mockTenant)).returns(() => Promise.resolve(mockFilteredSubscriptions));
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
- const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
+ const tenantTreeNodeLabel = `${mockTenant.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
+ const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredTenants.length} / ${mockTenants.length} tenants)`;
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
+ const tenantNodes = await accountTreeNode.getChildren();
- const subscriptionNodes = await accountTreeNode.getChildren();
+ should(tenantNodes).Array();
+ should(tenantNodes.length).equal(mockFilteredTenants.length);
- should(subscriptionNodes).Array();
- should(subscriptionNodes.length).equal(mockSubscriptions.length);
+ const tenantTreeNode = tenantNodes[0];
+ const subscriptionNodes = await tenantTreeNode.getChildren();
const treeItem = await accountTreeNode.getTreeItem();
should(treeItem.label).equal(accountTreeNodeLabel);
const nodeInfo = accountTreeNode.getNodeInfo();
should(nodeInfo.label).equal(accountTreeNodeLabel);
- });
-
- it('Should only show subscriptions with valid tokens for ADAL.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
- sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
- const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
-
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
-
- const subscriptionNodes = await accountTreeNode.getChildren();
-
- should(subscriptionNodes).Array();
- should(subscriptionNodes.length).equal(1);
-
- const treeItem = await accountTreeNode.getTreeItem();
- should(treeItem.label).equal(accountTreeNodeLabel);
-
- const nodeInfo = accountTreeNode.getNodeInfo();
- should(nodeInfo.label).equal(accountTreeNodeLabel);
- });
-
- it('Should only show subscriptions with valid tokens for MSAL.', async function (): Promise {
- mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
- sinon.stub(azdata.accounts, 'getAccountSecurityToken').onFirstCall().resolves(mockToken);
- const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
-
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
-
- const subscriptionNodes = await accountTreeNode.getChildren();
-
- should(subscriptionNodes).Array();
- should(subscriptionNodes.length).equal(1);
-
- 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 for ADAL.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
- sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
- const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
-
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
-
- const subscriptionNodes = await accountTreeNode.getChildren();
should(subscriptionNodes).Array();
should(subscriptionNodes.length).equal(mockFilteredSubscriptions.length);
- const treeItem = await accountTreeNode.getTreeItem();
- should(treeItem.label).equal(accountTreeNodeLabel);
+ const tenantTreeItem = await tenantTreeNode.getTreeItem();
+ should(tenantTreeItem.label).equal(tenantTreeNodeLabel);
- const nodeInfo = accountTreeNode.getNodeInfo();
- should(nodeInfo.label).equal(accountTreeNodeLabel);
- });
-
- it('Should be correct when there are subscriptions filtered for MSAL.', async function (): Promise {
- mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
- sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
- const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`;
-
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextMSAL, mockTreeChangeHandler.object);
-
- const subscriptionNodes = await accountTreeNode.getChildren();
-
- should(subscriptionNodes).Array();
- should(subscriptionNodes.length).equal(mockFilteredSubscriptions.length);
-
- const treeItem = await accountTreeNode.getTreeItem();
- should(treeItem.label).equal(accountTreeNodeLabel);
-
- const nodeInfo = accountTreeNode.getNodeInfo();
- should(nodeInfo.label).equal(accountTreeNodeLabel);
+ const tenantNodeInfo = tenantTreeNode.getNodeInfo();
+ should(tenantNodeInfo.label).equal(tenantTreeNodeLabel);
});
});
@@ -296,23 +279,18 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType();
mockCacheService = TypeMoq.Mock.ofType();
- mockSubscriptionServiceADAL = TypeMoq.Mock.ofType();
- mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType();
+ mockSubscriptionService = TypeMoq.Mock.ofType();
mockSubscriptionFilterService = TypeMoq.Mock.ofType();
mockTreeChangeHandler = TypeMoq.Mock.ofType();
mockSubscriptionCache = [];
- mockAppContextADAL = new AppContext(mockExtensionContext.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
-
- mockAppContextMSAL = new AppContext(mockExtensionContext.object);
- mockAppContextMSAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
- mockAppContextMSAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
- mockAppContextMSAL.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
+ mockAppContext = new AppContext(mockExtensionContext.object);
+ mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.tenantFilterService, mockTenantFilterService.object);
sinon.stub(azdata.accounts, 'getAccountSecurityToken').resolves(mockToken);
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
@@ -327,28 +305,27 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
sinon.restore();
});
- it('Should load subscriptions from scratch and update cache when it is clearing cache for ADAL.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
+ it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function (): Promise {
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
+ mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount, mockTenant)).returns(() => Promise.resolve([]));
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
+ const tenantTreeNode = new AzureResourceTenantTreeNode(mockAccount, mockTenant, accountTreeNode, mockAppContext, mockTreeChangeHandler.object);
+ const children = await tenantTreeNode.getChildren();
- const children = await accountTreeNode.getChildren();
-
- mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
+ mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(0));
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
- mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
+ mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
- mockTreeChangeHandler.verify((o) => o.notifyNodeChanged(accountTreeNode), TypeMoq.Times.once());
+ mockTreeChangeHandler.verify((o) => o.notifyNodeChanged(tenantTreeNode), TypeMoq.Times.once());
- should(accountTreeNode.totalSubscriptionCount).equal(mockSubscriptions.length);
- should(accountTreeNode.selectedSubscriptionCount).equal(mockSubscriptions.length);
- should(accountTreeNode.isClearingCache).false();
+ should(tenantTreeNode.totalSubscriptionCount).equal(mockSubscriptions.length);
+ should(tenantTreeNode.selectedSubscriptionCount).equal(mockSubscriptions.length);
+ should(tenantTreeNode.isClearingCache).false();
should(children).Array();
should(children.length).equal(mockSubscriptions.length);
-
should(mockSubscriptionCache).deepEqual(mockSubscriptions);
for (let ix = 0; ix < mockSubscriptions.length; ix++) {
@@ -356,39 +333,38 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
const subscription = mockSubscriptions[ix];
should(child).instanceof(AzureResourceSubscriptionTreeNode);
- should(child.nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${subscription.id}.tenant_${mockTenantId}`);
+ should(child.nodePathValue).equal(`account_${mockAccount.key.accountId}.tenant_${mockTenantId}.subscription_${subscription.id}`);
}
});
it('Should load subscriptions from cache when it is not clearing cache.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve([]));
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
+ mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount, mockTenant)).returns(() => Promise.resolve([]));
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
- await accountTreeNode.getChildren();
- const children = await accountTreeNode.getChildren();
+ const tenants = await accountTreeNode.getChildren();
+ await tenants[0].getChildren();
+ const children = await tenants[0].getChildren();
-
- mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
+ mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
should(children.length).equal(mockSubscriptionCache.length);
for (let ix = 0; ix < mockSubscriptionCache.length; ix++) {
- should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockSubscriptionCache[ix].id}.tenant_${mockTenantId}`);
+ should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.tenant_${mockTenantId}.subscription_${mockSubscriptionCache[ix].id}`);
}
});
it('Should handle when there is no subscriptions.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
+ const tenantTreeNode = new AzureResourceTenantTreeNode(mockAccount, mockTenant, accountTreeNode, mockAppContext, mockTreeChangeHandler.object);
+ const children = await tenantTreeNode.getChildren();
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
-
- const children = await accountTreeNode.getChildren();
-
- should(accountTreeNode.totalSubscriptionCount).equal(0);
+ should(tenantTreeNode.totalSubscriptionCount).equal(0);
should(children).Array();
should(children.length).equal(1);
@@ -397,36 +373,51 @@ describe('AzureResourceAccountTreeNode.getChildren', function (): void {
should(children[0].getNodeInfo().label).equal('No Subscriptions found.');
});
- it('Should honor subscription filtering.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions));
-
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
+ it('Should honor tenant filtering.', async function (): Promise {
+ mockTenantFilterService.setup((o) => o.getSelectedTenants(mockAccount)).returns(() => Promise.resolve(mockFilteredTenants));
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
const children = await accountTreeNode.getChildren();
- mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
+ should(accountTreeNode.selectedTenantCount).equal(mockFilteredTenants.length);
+ should(children.length).equal(mockFilteredTenants.length);
- should(accountTreeNode.selectedSubscriptionCount).equal(mockFilteredSubscriptions.length);
- should(children.length).equal(mockFilteredSubscriptions.length);
+ for (let ix = 0; ix < mockFilteredTenants.length; ix++) {
+ should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.tenant_${mockTenantId}`);
+ }
+ });
+
+ it('Should honor subscription filtering.', async function (): Promise {
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockFilteredSubscriptions));
+
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
+ const tenantTreeNode = new AzureResourceTenantTreeNode(mockAccount, mockTenant, accountTreeNode, mockAppContext, mockTreeChangeHandler.object);
+ const subscriptions = await tenantTreeNode.getChildren();
+
+ mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
+
+ should(subscriptions.length).equal(mockFilteredSubscriptions.length);
+ should(tenantTreeNode.selectedSubscriptionCount).equal(mockFilteredSubscriptions.length);
for (let ix = 0; ix < mockFilteredSubscriptions.length; ix++) {
- should(children[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockFilteredSubscriptions[ix].id}.tenant_${mockTenantId}`);
+ const subscription = mockSubscriptions[ix];
+ should(subscriptions[ix].nodePathValue).equal(`account_${mockAccount.key.accountId}.tenant_${mockTenantId}.subscription_${subscription.id}`);
}
});
it('Should handle errors.', async function (): Promise {
- mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
+ mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny())).returns(() => Promise.resolve(mockSubscriptions));
const mockError = 'Test error';
- mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => { throw new Error(mockError); });
+ mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount, mockTenant)).returns(() => { throw new Error(mockError); });
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
- const children = await accountTreeNode.getChildren();
+ const tenants = await accountTreeNode.getChildren();
+ const children = await tenants[0].getChildren();
- mockSubscriptionServiceADAL.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
- mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once());
+ mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, TypeMoq.It.isAny()), TypeMoq.Times.once());
+ mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount, mockTenant), 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.once());
@@ -442,17 +433,17 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
beforeEach(() => {
mockExtensionContext = TypeMoq.Mock.ofType();
mockCacheService = TypeMoq.Mock.ofType();
- mockSubscriptionServiceADAL = TypeMoq.Mock.ofType();
+ mockSubscriptionService = TypeMoq.Mock.ofType();
mockSubscriptionFilterService = TypeMoq.Mock.ofType();
mockTreeChangeHandler = TypeMoq.Mock.ofType();
mockSubscriptionCache = [];
- mockAppContextADAL = new AppContext(mockExtensionContext.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
- mockAppContextADAL.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
+ mockAppContext = new AppContext(mockExtensionContext.object);
+ mockAppContext.registerService(AzureResourceServiceNames.cacheService, mockCacheService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
+ mockAppContext.registerService(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
sinon.stub(azdata.accounts, 'getAccountSecurityToken').returns(Promise.resolve(mockToken));
mockCacheService.setup((o) => o.generateKey(TypeMoq.It.isAnyString())).returns(() => generateGuid());
@@ -468,7 +459,7 @@ describe('AzureResourceAccountTreeNode.clearCache', function (): void {
});
it('Should clear cache.', async function (): Promise {
- const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
+ const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
accountTreeNode.clearCache();
should(accountTreeNode.isClearingCache).true();
});
diff --git a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts
index cec3182f28..b5abd6aff2 100644
--- a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts
+++ b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts
@@ -17,7 +17,7 @@ import { AzureResourceService } from '../../../azureResource/resourceService';
import { AzureResourceResourceTreeNode } from '../../../azureResource/resourceTreeNode';
import { IAzureResourceCacheService } from '../../../azureResource/interfaces';
import { generateGuid } from '../../../azureResource/utils';
-import { AzureAccount, AzureAccountProperties, azureResource } from 'azurecore';
+import { AzureAccount, AzureAccountProperties, Tenant, azureResource } from 'azurecore';
import { TreeNode } from '../../../azureResource/treeNode';
// Mock services
@@ -47,6 +47,13 @@ const mockAccount: AzureAccount = {
const mockTenantId: string = 'mock_tenant';
const mockSubscriptionId: string = 'mock_subscription';
+const mockTenant: Tenant = {
+ id: mockTenantId,
+ displayName: 'mock_tenant',
+ userId: 'test@email.com',
+ tenantCategory: 'Home'
+}
+
const mockSubscription: azureResource.AzureResourceSubscription = {
id: mockSubscriptionId,
name: 'mock subscription',
@@ -96,9 +103,9 @@ describe('AzureResourceSubscriptionTreeNode.info', function (): void {
});
it('Should be correct when created.', async function (): Promise {
- const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType().object);
+ const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenant, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType().object);
- should(subscriptionTreeNode.nodePathValue).equal(`account_${mockAccount.key.accountId}.subscription_${mockSubscription.id}.tenant_${mockTenantId}`);
+ should(subscriptionTreeNode.nodePathValue).equal(`account_${mockAccount.key.accountId}.tenant_${mockTenantId}.subscription_${mockSubscription.id}`);
const treeItem = await subscriptionTreeNode.getTreeItem();
should(treeItem.label).equal(mockSubscription.name);
@@ -147,7 +154,7 @@ describe('AzureResourceSubscriptionTreeNode.getChildren', function (): void {
});
it('Should return resource containers.', async function (): Promise {
- const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType().object);
+ const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenant, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType().object);
const children = await subscriptionTreeNode.getChildren();
mockResourceTreeDataProvider1.verify((o) => o.getRootChildren(), TypeMoq.Times.once());
diff --git a/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css b/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css
index f72a84eca9..18442335f9 100644
--- a/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css
+++ b/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css
@@ -12,7 +12,8 @@
.list-row.account-picker-list .label,
.list-row.tenant-picker-list .label {
flex: 1 1 auto;
- margin-left: 12px;
+ margin-left: 8px;
+ margin-top: 6px;
overflow: hidden;
}