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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user