Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tenants view #869

Merged
merged 24 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/src/resources/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface AzureSubscription {
* The tenant to which this subscription belongs or undefined, if not associated with a specific tenant.
*/
readonly tenantId: string;

/**
* The account associated with this subscription. This is optional as we only need the account if there are duplicate subscriptions.
*/
readonly account?: vscode.AuthenticationSessionAccountInformation;
}

/**
Expand Down
32 changes: 16 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 46 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"icon": "resources/resourceGroup.png",
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"engines": {
"vscode": "^1.82.0"
"vscode": "^1.93.0"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -236,6 +236,24 @@
"command": "azureResourceGroups.showGroupOptions",
"title": "%azureResourceGroups.showGroupOptions%",
"category": "Azure"
},
{
"command": "azureTenantsView.refresh",
"title": "%azureResourceGroups.refresh%",
"category": "Azure",
"icon": "$(refresh)"
},
{
"command": "azureTenantsView.refreshTree",
"title": "%azureResourceGroups.refresh%",
"category": "Azure",
"icon": "$(refresh)"
},
{
"command": "azureTenantsView.signInToTenant",
"title": "%azureTenantsView.signInToTenant%",
"category": "Azure",
"icon": "$(sign-in)"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -272,6 +290,11 @@
"name": "Workspace",
"visibility": "visible"
},
{
"id": "azureTenantsView",
"name": "Accounts & Tenants",
"visibility": "collapsed"
},
{
"id": "ms-azuretools.helpAndFeedback",
"name": "%ms-azuretools.helpAndFeedback%",
Expand Down Expand Up @@ -355,6 +378,11 @@
"when": "view == azureWorkspace",
"group": "navigation@10"
},
{
"command": "azureTenantsView.refreshTree",
"when": "view == azureTenantsView",
"group": "navigation@10"
},
{
"command": "azureResourceGroups.clearActivities",
"when": "view == azureActivityLog",
Expand Down Expand Up @@ -431,6 +459,16 @@
"command": "azureResourceGroups.viewProperties",
"when": "view =~ /azureResourceGroups|azureFocusView/ && viewItem =~ /azureResourceGroup/",
"group": "9@1"
},
{
"command": "azureTenantsView.signInToTenant",
"when": "view == azureTenantsView && viewItem =~ /tenantNameNotSignedIn/",
"group": "inline@1"
},
{
"command": "azureTenantsView.signInToTenant",
"when": "view == azureTenantsView && viewItem =~ /tenantNameNotSignedIn/",
"group": "1@1"
}
],
"commandPalette": [
Expand All @@ -454,6 +492,10 @@
"command": "azureWorkspace.refresh",
"when": "never"
},
{
"command": "azureTenantsView.refresh",
"when": "never"
},
{
"command": "azureFocusView.refreshTree",
"when": "never"
Expand Down Expand Up @@ -688,15 +730,15 @@
"@azure/arm-resources-subscriptions": "^2.1.0",
"@azure/identity": "^4.2.1",
"@microsoft/api-extractor": "^7.33.8",
"@microsoft/eslint-config-azuretools": "^0.2.1",
"@microsoft/eslint-config-azuretools": "0.2.1",
"@microsoft/vscode-azext-dev": "^2.0.5",
"@types/gulp": "^4.0.6",
"@types/mocha": "^7.0.2",
"@types/node": "18.19.x",
"@types/request-promise": "^4.1.51",
"@types/semver": "^7.3.12",
"@types/uuid": "^9.0.1",
"@types/vscode": "^1.81.0",
"@types/vscode": "1.93.0",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@vscode/test-electron": "^2.3.8",
Expand All @@ -720,7 +762,7 @@
"@azure/arm-resources": "^5.2.0",
"@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0",
"@azure/ms-rest-js": "^2.7.0",
"@microsoft/vscode-azext-azureauth": "^2.5.0",
"@microsoft/vscode-azext-azureauth": "^3.1.0",
"@microsoft/vscode-azext-azureutils": "^3.1.1",
"@microsoft/vscode-azext-utils": "^2.5.11",
"buffer": "^6.0.3",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"azureResourceGroups.openInPortal": "Open in Portal",
"azureResourceGroups.refresh": "Refresh",
"azureWorkspace.refresh": "Refresh Workspace",
"azureTenantsView.refresh": "Refresh Accounts & Tenants",
"azureTenantsView.signInToTenant": "Sign in to Tenant",
"azureResourceGroups.selectedSubscriptions": "Selected Subscriptions",
"azureResourceGroups.revealResource": "Reveal Resource",
"azureResourceGroups.viewProperties": "View Properties",
Expand Down
4 changes: 4 additions & 0 deletions src/api/ResourceProviderManagers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as vscode from 'vscode';
import { AzureResource, AzureSubscription, ResourceBase, ResourceProvider, WorkspaceResource, WorkspaceResourceProvider } from '../../api/src/index';
import { AzureResourceProvider } from '../hostapi.v2.internal';
import { TenantResource, TenantResourceProvider } from '../tree/tenants/tenant';

export function isArray<T>(maybeArray: T[] | null | undefined): maybeArray is T[] {
return Array.isArray(maybeArray);
Expand Down Expand Up @@ -87,3 +88,6 @@ export class AzureResourceProviderManager extends ResourceProviderManager<AzureS

export class WorkspaceResourceProviderManager extends ResourceProviderManager<void, WorkspaceResource, WorkspaceResourceProvider> {
}

export class TenantResourceProviderManager extends ResourceProviderManager<void, TenantResource, TenantResourceProvider> {
}
2 changes: 2 additions & 0 deletions src/commands/accounts/logIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ export async function logIn(_context: IActionContext): Promise<void> {
const provider = await ext.subscriptionProviderFactory();
_isLoggingIn = true;
ext.actions.refreshAzureTree(); // Refresh to cause the "logging in" spinner to show
ext.actions.refreshTenantTree(); // Refresh to cause the "logging in" spinner to show
await provider.signIn();
} finally {
_isLoggingIn = false;
ext.actions.refreshAzureTree(); // Refresh now that sign in is complete
ext.actions.refreshTenantTree(); // Refresh now that sign in is complete
}
}

Expand Down
23 changes: 20 additions & 3 deletions src/commands/accounts/selectSubscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureSubscription } from "@microsoft/vscode-azext-azureauth";
import { IActionContext, IAzureQuickPickItem } from "@microsoft/vscode-azext-utils";
import { AzureSubscription } from "api/src/resources/azure";
import * as vscode from "vscode";
import { ext } from "../../extensionVariables";
import { isTenantFilteredOut } from "../../tree/tenants/registerTenantTree";
import { localize } from "../../utils/localize";
import { settingUtils } from "../../utils/settingUtils";

Expand All @@ -31,14 +32,15 @@ export async function selectSubscriptions(context: IActionContext, options?: Sel
let subscriptionsShownInPicker: string[] = [];

alexweininger marked this conversation as resolved.
Show resolved Hide resolved
const subscriptionQuickPickItems: () => Promise<IAzureQuickPickItem<AzureSubscription>[]> = async () => {

// If there are no tenants selected by default all subscriptions will be shown.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, but I want to discuss ways we could convey this behavior to users. When you only have a single sub it seems like checking/unchecking tenants does nothing (since the single sub will always be shown).

const allSubscriptions = await provider.getSubscriptions(false);
const subscriptionsFilteredByTenant = options?.tenantId ? allSubscriptions.filter(subscription => subscription.tenantId === options.tenantId) : allSubscriptions;
const duplicates = getDuplicateSubscriptions(allSubscriptions);

subscriptionsShownInPicker = subscriptionsFilteredByTenant.map(sub => `${sub.tenantId}/${sub.subscriptionId}`);
return subscriptionsFilteredByTenant
.map(subscription => ({
label: subscription.name,
label: duplicates.includes(subscription) ? subscription.name + ` (${subscription.account?.label})` : subscription.name,
description: subscription.subscriptionId,
data: subscription
}))
Expand Down Expand Up @@ -95,3 +97,18 @@ export async function getSelectedTenantAndSubscriptionIds(): Promise<string[]> {
async function setSelectedTenantAndSubscriptionIds(tenantAndSubscriptionIds: string[]): Promise<void> {
await settingUtils.updateGlobalSetting('selectedSubscriptions', tenantAndSubscriptionIds);
}

// This function is also used to filter subscription tree items in AzureResourceTreeDataProvider
export function getTenantFilteredSubscriptions(allSubscriptions: AzureSubscription[]): AzureSubscription[] | undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what this function is doing, and why it returns undefined instead an empty array. I would either refactor or make it more clear that this actually matters later on in the UI logic.

I wouldn't want someone to refactor this based on the function name and then mess things up.

const filteredSubscriptions = allSubscriptions.filter(subscription => !isTenantFilteredOut(subscription.tenantId, subscription.account.id));
return filteredSubscriptions.length > 0 ? filteredSubscriptions : allSubscriptions;
}

export function getDuplicateSubscriptions(subscriptions: AzureSubscription[]): AzureSubscription[] {
const lookup = subscriptions.reduce((accumulator, sub) => {
accumulator[sub.subscriptionId] = ++accumulator[sub.subscriptionId] || 0;
return accumulator;
}, {} as Record<string, number>);

return subscriptions.filter(sub => lookup[sub.subscriptionId]);
}
14 changes: 13 additions & 1 deletion src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

import { signInToTenant } from '@microsoft/vscode-azext-azureauth';
import { AzExtTreeItem, IActionContext, isAzExtTreeItem, openUrl, registerCommand, registerErrorHandler, registerReportIssueCommand } from '@microsoft/vscode-azext-utils';
import { commands } from 'vscode';
import { AuthenticationSessionAccountInformation, commands } from 'vscode';
import { uploadFileToCloudShell } from '../cloudConsole/uploadFileToCloudShell';
import { ext } from '../extensionVariables';
import { BranchDataItemWrapper } from '../tree/BranchDataItemWrapper';
import { ResourceGroupsItem } from '../tree/ResourceGroupsItem';
import { GroupingItem } from '../tree/azure/grouping/GroupingItem';
import { TenantTreeItem } from '../tree/tenants/TenantTreeItem';
import { logIn } from './accounts/logIn';
import { SelectSubscriptionOptions, selectSubscriptions } from './accounts/selectSubscriptions';
import { clearActivities } from './activities/clearActivities';
Expand Down Expand Up @@ -40,6 +41,7 @@ export function registerCommands(): void {
registerCommand('azureResourceGroups.refreshTree', () => ext.actions.refreshAzureTree());
registerCommand('azureWorkspace.refreshTree', () => ext.actions.refreshWorkspaceTree());
registerCommand('azureFocusView.refreshTree', () => ext.actions.refreshFocusTree());
registerCommand('azureTenantsView.refreshTree', () => ext.actions.refreshTenantTree());

// v1.5 client extensions attach these commands to tree item context menus for refreshing their tree items
registerCommand('azureResourceGroups.refresh', async (context, node?: ResourceGroupsItem) => {
Expand All @@ -63,6 +65,16 @@ export function registerCommands(): void {
ext.actions.refreshFocusTree(node);
});

registerCommand('azureTenantsView.refresh', async (context, node?: ResourceGroupsItem) => {
await handleAzExtTreeItemRefresh(context, node); // for compatibility with v1.5 client extensions
motm32 marked this conversation as resolved.
Show resolved Hide resolved
ext.actions.refreshTenantTree(node);
});

registerCommand('azureTenantsView.signInToTenant', async (_context, node: TenantTreeItem, account?: AuthenticationSessionAccountInformation) => {
await (await ext.subscriptionProviderFactory()).signIn(node.tenantId, account);
ext.actions.refreshTenantTree(node);
});

registerCommand('azureResourceGroups.focusGroup', focusGroup);
registerCommand('azureResourceGroups.unfocusGroup', unfocusGroup);

Expand Down
Loading
Loading