mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Introduce Tenant hierarchy in Azure resource tree for multi-tenant accounts (#23311)
This commit is contained in:
@@ -194,7 +194,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> {
|
||||
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<Tenant[]> {
|
||||
public async getTenantsMsal(token: string, tokenClaims: TokenClaims): Promise<Tenant[]> {
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -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<IAzureResourceTenantFilterService>(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) => {
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -135,9 +135,14 @@ export interface IAzureResourceSubscriptionService {
|
||||
getSubscriptions(account: AzureAccount, tenantIds?: string[] | undefined): Promise<azureResource.AzureResourceSubscription[]>;
|
||||
}
|
||||
|
||||
export interface IAzureResourceTenantFilterService {
|
||||
getSelectedTenants(account: AzureAccount): Promise<Tenant[]>;
|
||||
saveSelectedTenants(account: AzureAccount, selectedTenants: Tenant[]): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IAzureResourceSubscriptionFilterService {
|
||||
getSelectedSubscriptions(account: AzureAccount): Promise<azureResource.AzureResourceSubscription[]>;
|
||||
saveSelectedSubscriptions(account: AzureAccount, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise<void>;
|
||||
getSelectedSubscriptions(account: AzureAccount, tenant: Tenant): Promise<azureResource.AzureResourceSubscription[]>;
|
||||
saveSelectedSubscriptions(account: AzureAccount, tenant: Tenant, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IAzureTerminalService {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
|
||||
|
||||
export class AzureMonitorTreeDataProvider extends ResourceTreeDataProviderBase<GraphData, AzureMonitorGraphData> {
|
||||
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<G
|
||||
|
||||
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
|
||||
return {
|
||||
id: `${AzureResourcePrefixes.logAnalytics}${account.key.accountId}${databaseServer.id ? databaseServer.id : databaseServer.name}`,
|
||||
id: `${AzureResourcePrefixes.logAnalytics}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ? databaseServer.id : databaseServer.name}`,
|
||||
label: this.browseConnectionMode ? `${databaseServer.name} (${AzureMonitorTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name,
|
||||
iconPath: {
|
||||
dark: this._extensionContext.asAbsolutePath('resources/dark/azure_monitor_dark.svg'),
|
||||
light: this._extensionContext.asAbsolutePath('resources/light/azure_monitor_light.svg')
|
||||
},
|
||||
iconPath: this._extensionContext.asAbsolutePath('resources/logAnalyticsWorkspaces.svg'),
|
||||
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.azureMonitor,
|
||||
payload: {
|
||||
@@ -63,10 +60,7 @@ export class AzureMonitorTreeDataProvider extends ResourceTreeDataProviderBase<G
|
||||
return [{
|
||||
id: AzureMonitorTreeDataProvider.containerId,
|
||||
label: AzureMonitorTreeDataProvider.containerLabel,
|
||||
iconPath: {
|
||||
dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
|
||||
light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
|
||||
},
|
||||
iconPath: this._extensionContext.asAbsolutePath('resources/logAnalyticsWorkspaces.svg'),
|
||||
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.databaseServerContainer
|
||||
}];
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as azdata from 'azdata';
|
||||
|
||||
export class CosmosDbMongoTreeDataProvider extends ResourceTreeDataProviderBase<GraphData, DbServerGraphData> {
|
||||
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
|
||||
}];
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AzureAccount, azureResource } from 'azurecore';
|
||||
export class AzureResourceDatabaseTreeDataProvider extends ResourceTreeDataProviderBase<DbServerGraphData, DatabaseGraphData> {
|
||||
|
||||
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
|
||||
}];
|
||||
|
||||
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
|
||||
|
||||
export class AzureResourceDatabaseServerTreeDataProvider extends ResourceTreeDataProviderBase<GraphData, DbServerGraphData> {
|
||||
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
|
||||
}];
|
||||
|
||||
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
|
||||
|
||||
export class KustoTreeDataProvider extends ResourceTreeDataProviderBase<GraphData, KustoGraphData> {
|
||||
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<GraphDat
|
||||
|
||||
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
|
||||
return {
|
||||
id: `${AzureResourcePrefixes.kusto}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
|
||||
id: `${AzureResourcePrefixes.kusto}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
|
||||
label: this.browseConnectionMode ? `${databaseServer.name} (${KustoTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name,
|
||||
iconPath: {
|
||||
dark: this._extensionContext.asAbsolutePath('resources/dark/azureDE_inverse.svg'),
|
||||
light: this._extensionContext.asAbsolutePath('resources/light/azureDE.svg')
|
||||
},
|
||||
iconPath: this._extensionContext.asAbsolutePath('resources/dataExplorerClusterDb.svg'),
|
||||
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.azureDataExplorer,
|
||||
payload: {
|
||||
@@ -63,10 +60,7 @@ export class KustoTreeDataProvider extends ResourceTreeDataProviderBase<GraphDat
|
||||
return [{
|
||||
id: KustoTreeDataProvider.containerId,
|
||||
label: KustoTreeDataProvider.containerLabel,
|
||||
iconPath: {
|
||||
dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
|
||||
light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
|
||||
},
|
||||
iconPath: this._extensionContext.asAbsolutePath('resources/dataExplorerClusterDb.svg'),
|
||||
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.databaseServerContainer
|
||||
}];
|
||||
|
||||
@@ -27,12 +27,9 @@ export class MysqlFlexibleServerTreeDataProvider extends ResourceTreeDataProvide
|
||||
|
||||
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: Account): TreeItem {
|
||||
return {
|
||||
id: `${AzureResourcePrefixes.mySqlFlexibleServer}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
|
||||
id: `${AzureResourcePrefixes.mySqlFlexibleServer}${account.key.accountId}${databaseServer.tenant}${databaseServer.id ?? databaseServer.name}`,
|
||||
label: this.browseConnectionMode ? `${databaseServer.name} (${MysqlFlexibleServerTreeDataProvider.CONTAINER_LABEL}, ${databaseServer.subscription.name})` : databaseServer.name,
|
||||
iconPath: {
|
||||
dark: this._extensionContext.asAbsolutePath('resources/dark/mysql_server_inverse.svg'),
|
||||
light: this._extensionContext.asAbsolutePath('resources/light/mysql_server.svg')
|
||||
},
|
||||
iconPath: this._extensionContext.asAbsolutePath('resources/mysqlDatabase.svg'),
|
||||
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.databaseServer,
|
||||
payload: {
|
||||
@@ -64,10 +61,7 @@ export class MysqlFlexibleServerTreeDataProvider extends ResourceTreeDataProvide
|
||||
return [{
|
||||
id: MysqlFlexibleServerTreeDataProvider.CONTAINER_ID,
|
||||
label: MysqlFlexibleServerTreeDataProvider.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/mysqlDatabase.svg'),
|
||||
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.databaseServerContainer
|
||||
}];
|
||||
|
||||
@@ -17,7 +17,7 @@ import { GraphData, PostgresArcServerGraphData } from '../../interfaces';
|
||||
export class PostgresServerArcTreeDataProvider extends ResourceTreeDataProviderBase<GraphData, PostgresArcServerGraphData> {
|
||||
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
|
||||
}];
|
||||
|
||||
@@ -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
|
||||
}];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ import { AzureAccount, azureResource } from 'azurecore';
|
||||
|
||||
export class SqlInstanceTreeDataProvider extends ResourceTreeDataProviderBase<GraphData, SqlInstanceGraphData> {
|
||||
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<Gr
|
||||
|
||||
public getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: AzureAccount): TreeItem {
|
||||
return {
|
||||
id: `${AzureResourcePrefixes.sqlInstance}${account.key.accountId}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
|
||||
id: `${AzureResourcePrefixes.sqlInstance}${account.key.accountId}${databaseServer.tenant}${account.key.accountId}${databaseServer.id ?? databaseServer.name}`,
|
||||
label: this.browseConnectionMode ? `${databaseServer.name} (${SqlInstanceTreeDataProvider.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/sqlManagedInstance.svg'),
|
||||
collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.databaseServer,
|
||||
payload: {
|
||||
@@ -63,10 +60,7 @@ export class SqlInstanceTreeDataProvider extends ResourceTreeDataProviderBase<Gr
|
||||
return [{
|
||||
id: SqlInstanceTreeDataProvider.containerId,
|
||||
label: SqlInstanceTreeDataProvider.containerLabel,
|
||||
iconPath: {
|
||||
dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
|
||||
light: this._extensionContext.asAbsolutePath('resources/light/folder.svg')
|
||||
},
|
||||
iconPath: this._extensionContext.asAbsolutePath('resources/sqlManagedInstance.svg'),
|
||||
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: AzureResourceItemType.databaseServerContainer
|
||||
}];
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AzureAccount, azureResource } from 'azurecore';
|
||||
export class SqlInstanceArcTreeDataProvider extends ResourceTreeDataProviderBase<GraphData, SqlInstanceArcGraphData> {
|
||||
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
|
||||
}];
|
||||
|
||||
@@ -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
|
||||
}];
|
||||
|
||||
@@ -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
|
||||
}];
|
||||
|
||||
@@ -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<azureResource.AzureResourceSubscription[]> {
|
||||
public async getSelectedSubscriptions(account: AzureAccount, tenant: Tenant): Promise<azureResource.AzureResourceSubscription[]> {
|
||||
let selectedSubscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
|
||||
const cache = this._cacheService.get<AzureResourceSelectedSubscriptionsCache>(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<void> {
|
||||
let selectedSubscriptionsCache: { [accountId: string]: azureResource.AzureResourceSubscription[] } = {};
|
||||
public async saveSelectedSubscriptions(account: AzureAccount, tenant: Tenant, selectedSubscriptions: azureResource.AzureResourceSubscription[]): Promise<void> {
|
||||
let selections: { [accountTenantId: string]: azureResource.AzureResourceSubscription[] } = {};
|
||||
|
||||
const cache = this._cacheService.get<AzureResourceSelectedSubscriptionsCache>(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<AzureResourceSelectedSubscriptionsCache>(this._cacheKey, { selectedSubscriptions: selectedSubscriptionsCache });
|
||||
selections[accountTenantId] = selectedSubscriptions;
|
||||
|
||||
await this._cacheService.update<AzureResourceSelectedSubscriptionsCache>(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}`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Tenant[]> {
|
||||
let selectedTenants: Tenant[] = [];
|
||||
|
||||
const cache = this._cacheService.get<AzureResourceSelectedTenantsCache>(this._cacheKey);
|
||||
if (cache) {
|
||||
selectedTenants = cache.selectedTenants[account.key.accountId];
|
||||
}
|
||||
|
||||
return selectedTenants;
|
||||
}
|
||||
|
||||
public async saveSelectedTenants(account: AzureAccount, selectedTenants: Tenant[]): Promise<void> {
|
||||
let selectedTenantsCache: { [accountId: string]: Tenant[] } = {};
|
||||
|
||||
const cache = this._cacheService.get<AzureResourceSelectedTenantsCache>(this._cacheKey);
|
||||
if (cache) {
|
||||
selectedTenantsCache = cache.selectedTenants;
|
||||
}
|
||||
|
||||
if (!selectedTenantsCache) {
|
||||
selectedTenantsCache = {};
|
||||
}
|
||||
|
||||
selectedTenantsCache[account.key.accountId] = selectedTenants;
|
||||
|
||||
await this._cacheService.update<AzureResourceSelectedTenantsCache>(this._cacheKey, { selectedTenants: selectedTenantsCache });
|
||||
|
||||
const filters: string[] = [];
|
||||
for (const accountId in selectedTenantsCache) {
|
||||
filters.push(...selectedTenantsCache[accountId].map((tenant) => `${accountId}/${tenant.id}/${tenant.displayName}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IAzureResourceTenantFilterService>(AzureResourceServiceNames.tenantFilterService);
|
||||
|
||||
this._subscriptionService = this.appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
||||
this._subscriptionFilterService = this.appContext.getService<IAzureResourceSubscriptionFilterService>(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<TreeNode[]> {
|
||||
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<azureResource.AzureResourceSubscription[]>(subscriptions);
|
||||
this._isClearingCache = false;
|
||||
} else {
|
||||
subscriptions = await this.getCachedSubscriptions();
|
||||
}
|
||||
const selectedTenants = await this._tenantFilterService.getSelectedTenants(this.account);
|
||||
const selectedTenantIds = (selectedTenants || <Tenant[]>[]).map((Tenant) => Tenant.id);
|
||||
|
||||
this._totalSubscriptionCount = subscriptions.length;
|
||||
|
||||
const selectedSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account);
|
||||
const selectedSubscriptionIds = (selectedSubscriptions || <azureResource.AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
|
||||
if (selectedSubscriptionIds.length > 0) {
|
||||
subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
|
||||
this._selectedSubscriptionCount = selectedSubscriptionIds.length;
|
||||
} 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<azureResource.AzureResourceSubscription[]> {
|
||||
return this.getCache<azureResource.AzureResourceSubscription[]>() ?? [];
|
||||
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<vscode.TreeItem> {
|
||||
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.");
|
||||
|
||||
}
|
||||
|
||||
@@ -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<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TreeNode>, IAzureResourceTreeChangeHandler {
|
||||
public isSystemInitialized: boolean = false;
|
||||
@@ -45,7 +45,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
|
||||
|
||||
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
||||
if (element) {
|
||||
return element.getChildren(true);
|
||||
return element.getChildren();
|
||||
}
|
||||
|
||||
if (!this.isSystemInitialized) {
|
||||
@@ -63,6 +63,7 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
|
||||
for (const account of accounts) {
|
||||
try {
|
||||
const accountNode = new FlatAccountTreeNode(account, this.appContext, this);
|
||||
accountNode.refreshLabel();
|
||||
accountNodes.push(accountNode);
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@@ -11,17 +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 { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
|
||||
import { AzureAccount, azureResource } from 'azurecore';
|
||||
import { AzureResourceService } from '../resourceService';
|
||||
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
|
||||
import { AzureResourceErrorMessageUtil } from '../utils';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
import { IAzureResourceTenantFilterService } from '../interfaces';
|
||||
import { AzureAccount, Tenant } from 'azurecore';
|
||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { FlatTenantTreeNode } from './flatTenantTreeNode';
|
||||
|
||||
export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
||||
public constructor(
|
||||
@@ -31,60 +27,69 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
||||
) {
|
||||
super(appContext, treeChangeHandler, undefined);
|
||||
|
||||
this._subscriptionService = this.appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
||||
this._subscriptionFilterService = this.appContext.getService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
|
||||
this._resourceService = this.appContext.getService<AzureResourceService>(AzureResourceServiceNames.resourceService);
|
||||
this._tenantFilterService = this.appContext.getService<IAzureResourceTenantFilterService>(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<void> {
|
||||
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<TreeNode[]> {
|
||||
let nodesResult: TreeNode[] = [];
|
||||
let tenants = this.account.properties.tenants;
|
||||
this._totalTenantsCount = tenants.length;
|
||||
|
||||
const selectedTenants = await this._tenantFilterService.getSelectedTenants(this.account);
|
||||
const selectedTenantIds = (selectedTenants || <Tenant[]>[]).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<vscode.TreeItem> {
|
||||
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 || <azureResource.AzureResourceSubscription[]>[]).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<void>();
|
||||
public readonly onNewResourcesAvailable = this._onNewResourcesAvailable.event;
|
||||
private readonly _onLoadingStatusChanged = new vscode.EventEmitter<void>();
|
||||
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<void> {
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -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<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
||||
this._subscriptionFilterService = this.appContext.getService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
|
||||
this._resourceService = this.appContext.getService<AzureResourceService>(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<TreeNode[]> {
|
||||
try {
|
||||
let nodesResult: TreeNode[] = [];
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
if (this._isClearingCache) {
|
||||
subscriptions = await this._subscriptionService.getSubscriptions(this.account, [this.tenant.id]);
|
||||
await this.updateCache<azureResource.AzureResourceSubscription[]>(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 || <azureResource.AzureResourceSubscription[]>[]).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<azureResource.AzureResourceSubscription[]> {
|
||||
return this.getCache<azureResource.AzureResourceSubscription[]>() ?? [];
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
|
||||
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 || <azureResource.AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
|
||||
if (selectedSubscriptionIds.length > 0) {
|
||||
subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
|
||||
selected = selectedSubscriptionIds.length;
|
||||
}
|
||||
return {
|
||||
subscriptions,
|
||||
total,
|
||||
selected
|
||||
};
|
||||
}
|
||||
@@ -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<TreeItem> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
156
extensions/azurecore/src/azureResource/tree/tenantTreeNode.ts
Normal file
156
extensions/azurecore/src/azureResource/tree/tenantTreeNode.ts
Normal file
@@ -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<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
|
||||
this._subscriptionFilterService = this.appContext.getService<IAzureResourceSubscriptionFilterService>(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<TreeNode[]> {
|
||||
try {
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
|
||||
if (this._isClearingCache) {
|
||||
subscriptions = await this._subscriptionService.getSubscriptions(this.account, [this.tenant.id]);
|
||||
await this.updateCache<azureResource.AzureResourceSubscription[]>(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 || <azureResource.AzureResourceSubscription[]>[]).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<azureResource.AzureResourceSubscription[]> {
|
||||
return this.getCache<azureResource.AzureResourceSubscription[]>() ?? [];
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
|
||||
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
||||
if (element) {
|
||||
return element.getChildren(true);
|
||||
return element.getChildren();
|
||||
}
|
||||
|
||||
if (!this.isSystemInitialized) {
|
||||
@@ -55,11 +55,15 @@ export class AzureResourceTreeProvider implements vscode.TreeDataProvider<TreeNo
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.accounts && this.accounts.length > 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<TreeNo
|
||||
|
||||
private async loadAccounts(): Promise<void> {
|
||||
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);
|
||||
|
||||
@@ -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<TreeNode[]>;
|
||||
public abstract getChildren(): TreeNode[] | Promise<TreeNode[]>;
|
||||
public abstract getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem>;
|
||||
|
||||
public abstract getNodeInfo(): azdata.NodeInfo;
|
||||
|
||||
@@ -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<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
|
||||
const tenantFilterService = appContext.getService<IAzureResourceTenantFilterService>(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,
|
||||
|
||||
@@ -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<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, new AzureResourceCacheService(extensionContext));
|
||||
appContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService());
|
||||
appContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(extensionContext)));
|
||||
appContext.registerService<IAzureResourceTenantFilterService>(AzureResourceServiceNames.tenantFilterService, new AzureResourceTenantFilterService(new AzureResourceCacheService(extensionContext)));
|
||||
appContext.registerService<IAzureTerminalService>(AzureResourceServiceNames.terminalService, new AzureTerminalService(extensionContext));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<vscode.ExtensionContext>;
|
||||
let mockCacheService: TypeMoq.IMock<IAzureResourceCacheService>;
|
||||
let mockSubscriptionServiceADAL: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
||||
let mockSubscriptionServiceMSAL: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
||||
let mockSubscriptionService: TypeMoq.IMock<IAzureResourceSubscriptionService>;
|
||||
let mockSubscriptionFilterService: TypeMoq.IMock<IAzureResourceSubscriptionFilterService>;
|
||||
let mockAppContextADAL: AppContext;
|
||||
let mockAppContextMSAL: AppContext;
|
||||
let mockTenantFilterService: TypeMoq.IMock<IAzureResourceTenantFilterService>;
|
||||
let mockAppContext: AppContext;
|
||||
let mockTreeChangeHandler: TypeMoq.IMock<IAzureResourceTreeChangeHandler>;
|
||||
|
||||
// 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<vscode.ExtensionContext>();
|
||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceADAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceMSAL.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount)).returns(() => Promise.resolve(mockSubscriptions));
|
||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||
mockTenantFilterService = TypeMoq.Mock.ofType<IAzureResourceTenantFilterService>();
|
||||
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||
|
||||
mockSubscriptionCache = [];
|
||||
|
||||
mockAppContextADAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
|
||||
mockAppContextMSAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContext = new AppContext(mockExtensionContext.object);
|
||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContext.registerService<IAzureResourceTenantFilterService>(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<void> {
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
it('Should be correct when created.', async function (): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<vscode.ExtensionContext>();
|
||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionServiceMSAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||
|
||||
mockSubscriptionCache = [];
|
||||
|
||||
mockAppContextADAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
|
||||
mockAppContextMSAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceMSAL.object);
|
||||
mockAppContextMSAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContext = new AppContext(mockExtensionContext.object);
|
||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContext.registerService<IAzureResourceTenantFilterService>(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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<vscode.ExtensionContext>();
|
||||
mockCacheService = TypeMoq.Mock.ofType<IAzureResourceCacheService>();
|
||||
mockSubscriptionServiceADAL = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionService>();
|
||||
mockSubscriptionFilterService = TypeMoq.Mock.ofType<IAzureResourceSubscriptionFilterService>();
|
||||
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<IAzureResourceTreeChangeHandler>();
|
||||
|
||||
mockSubscriptionCache = [];
|
||||
|
||||
mockAppContextADAL = new AppContext(mockExtensionContext.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionServiceADAL.object);
|
||||
mockAppContextADAL.registerService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService, mockSubscriptionFilterService.object);
|
||||
mockAppContext = new AppContext(mockExtensionContext.object);
|
||||
mockAppContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, mockCacheService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, mockSubscriptionService.object);
|
||||
mockAppContext.registerService<IAzureResourceSubscriptionFilterService>(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<void> {
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContextADAL, mockTreeChangeHandler.object);
|
||||
const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockAppContext, mockTreeChangeHandler.object);
|
||||
accountTreeNode.clearCache();
|
||||
should(accountTreeNode.isClearingCache).true();
|
||||
});
|
||||
|
||||
@@ -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<void> {
|
||||
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType<TreeNode>().object);
|
||||
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenant, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType<TreeNode>().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<void> {
|
||||
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenantId, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType<TreeNode>().object);
|
||||
const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockAccount, mockSubscription, mockTenant, appContext, mockTreeChangeHandler.object, TypeMoq.Mock.ofType<TreeNode>().object);
|
||||
const children = await subscriptionTreeNode.getChildren();
|
||||
|
||||
mockResourceTreeDataProvider1.verify((o) => o.getRootChildren(), TypeMoq.Times.once());
|
||||
|
||||
Reference in New Issue
Block a user