From 351bc43b698ab4d802e49dcb2d98ed600f74bfc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:47:18 +0000 Subject: [PATCH 1/6] Initial plan From 1c870fbfdd54bc5653ef8f8a1f47d5f77ec157f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:59:39 +0000 Subject: [PATCH 2/6] Add support for generic tree items in empty resource type groups - Added `getGenericItemsForEmptyGroup()` method to `GroupingItem` class - Implemented override in `ResourceTypeGroupingItem` to show "Install extension" item - Added `supportsResourceType()` method to `AzExtWrapper` for checking resource type support - Added test for empty resource type groups showing install extension item - Modified `GroupingItem.getChildren()` to call generic items method when no resources Co-authored-by: alexweininger <12476526+alexweininger@users.noreply.github.com> --- src/AzExtWrapper.ts | 4 +++ src/tree/azure/grouping/GroupingItem.ts | 13 +++++++++ .../grouping/ResourceTypeGroupingItem.ts | 29 +++++++++++++++++++ test/grouping.test.ts | 29 +++++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/src/AzExtWrapper.ts b/src/AzExtWrapper.ts index 9240736d..61d4526c 100644 --- a/src/AzExtWrapper.ts +++ b/src/AzExtWrapper.ts @@ -63,6 +63,10 @@ export class AzExtWrapper { return this._resourceTypes.some(rt => rt === resource.resourceType); } + public supportsResourceType(resourceType: AzExtResourceType | string): boolean { + return this._resourceTypes.some(rt => rt === resourceType); + } + public getCodeExtension(): Extension | undefined { return extensions.getExtension(this.id); } diff --git a/src/tree/azure/grouping/GroupingItem.ts b/src/tree/azure/grouping/GroupingItem.ts index 91867b47..65da81f0 100644 --- a/src/tree/azure/grouping/GroupingItem.ts +++ b/src/tree/azure/grouping/GroupingItem.ts @@ -74,6 +74,14 @@ export class GroupingItem implements ResourceGroupsItem { return resources; } + /** + * Get generic items to display when there are no resources. + * Can be overridden by subclasses to provide custom items. + */ + getGenericItemsForEmptyGroup(): ResourceGroupsItem[] | undefined { + return undefined; + } + async getChildren(): Promise { const sortedResources = this.getResourcesToDisplay(this.resources).sort((a, b) => { @@ -85,6 +93,11 @@ export class GroupingItem implements ResourceGroupsItem { return a.name.localeCompare(b.name); }); + // If there are no resources, check if there are generic items to display + if (sortedResources.length === 0) { + return this.getGenericItemsForEmptyGroup(); + } + const subscriptionGroupingMap = new Map(); sortedResources.forEach(resource => { diff --git a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts index ae83b4e6..3d4f6abd 100644 --- a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts +++ b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts @@ -3,8 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import { AzExtResourceType } from "api/src/AzExtResourceType"; +import { getAzureExtensions } from "../../../AzExtWrapper"; import { canFocusContextValue } from "../../../constants"; +import { localize } from "../../../utils/localize"; +import { GenericItem } from "../../GenericItem"; +import { ResourceGroupsItem } from "../../ResourceGroupsItem"; import { GroupingItem, GroupingItemOptions } from "./GroupingItem"; import { GroupingItemFactoryOptions } from "./GroupingItemFactory"; @@ -18,6 +23,30 @@ export class ResourceTypeGroupingItem extends GroupingItem { this.contextValues.push('azureResourceTypeGroup', resourceType, canFocusContextValue); } + + override getGenericItemsForEmptyGroup(): ResourceGroupsItem[] | undefined { + // Find the extension for this resource type + const extension = getAzureExtensions().find(ext => + ext.supportsResourceType(this.resourceType) + ); + + // If the extension is not installed and is not private, show an "Install extension" item + if (extension && !extension.isInstalled() && !extension.isPrivate()) { + return [ + new GenericItem( + localize('openInExtension', 'Open in {0} Extension', extension.label), + { + commandArgs: [extension.id], + commandId: 'azureResourceGroups.installExtension', + contextValue: 'installExtension', + iconPath: new vscode.ThemeIcon('extensions'), + id: `${this.id}/installExtension` + }) + ]; + } + + return undefined; + } } export function isResourceTypeGroupingItem(groupingItem: GroupingItem): groupingItem is ResourceTypeGroupingItem { diff --git a/test/grouping.test.ts b/test/grouping.test.ts index ec18665e..d5533720 100644 --- a/test/grouping.test.ts +++ b/test/grouping.test.ts @@ -5,6 +5,7 @@ import { GroupingItem } from "../src/tree/azure/grouping/GroupingItem"; import { isLocationGroupingItem, LocationGroupingItem } from "../src/tree/azure/grouping/LocationGroupingItem"; import { isResourceGroupGroupingItem, ResourceGroupGroupingItem } from "../src/tree/azure/grouping/ResourceGroupGroupingItem"; import { isResourceTypeGroupingItem, ResourceTypeGroupingItem } from "../src/tree/azure/grouping/ResourceTypeGroupingItem"; +import { GenericItem } from "../src/tree/GenericItem"; import { createMockSubscriptionWithFunctions } from "./api/mockServiceFactory"; import { getCachedTestApi } from "./utils/testApiAccess"; @@ -54,4 +55,32 @@ suite('Azure resource grouping tests', async () => { const locationGroup = groups.find(group => (group as LocationGroupingItem).location === mocks.sub1.rg1.location); assert.ok(locationGroup); }); + + test('Resource type group with no resources shows install extension item', async () => { + createMockSubscriptionWithFunctions(); + + await commands.executeCommand('azureResourceGroups.groupBy.resourceType'); + + const tdp = api().azureResourceTreeDataProvider; + const subscriptions = await tdp.getChildren(); + + const groups = await tdp.getChildren(subscriptions![0]) as GroupingItem[]; + + // Find a resource type group that has no resources (e.g., AiFoundry) + const aiFoundryGroup = groups.find(group => + isResourceTypeGroupingItem(group) && + (group as ResourceTypeGroupingItem).resourceType === AzExtResourceType.AiFoundry + ); + + if (aiFoundryGroup) { + const children = await aiFoundryGroup.getChildren(); + + // Should have at least one child (the "Install extension" item) + assert.ok(children && children.length > 0, 'Expected install extension item for empty resource type group'); + + // First child should be a GenericItem with the correct command + const firstChild = children[0]; + assert.ok(firstChild instanceof GenericItem, 'Expected first child to be a GenericItem'); + } + }); }); From 0053c5326aba0bb6b64fa98dedd297f5686e1827 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:13:39 +0000 Subject: [PATCH 3/6] Update AI Foundry empty group to open Foundry view instead of install page - Changed behavior for AI Foundry resource type when extension is installed - Now opens microsoft-foundry-resources view using .focus command - Updated icon from 'extensions' to 'link-external' for clarity - Updated test to account for both install and open behaviors Co-authored-by: alexweininger <12476526+alexweininger@users.noreply.github.com> --- .../grouping/ResourceTypeGroupingItem.ts | 21 ++++++++++++++++++- test/grouping.test.ts | 8 +++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts index 3d4f6abd..991cdf32 100644 --- a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts +++ b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts @@ -30,8 +30,27 @@ export class ResourceTypeGroupingItem extends GroupingItem { ext.supportsResourceType(this.resourceType) ); + if (!extension || extension.isPrivate()) { + return undefined; + } + + // Special handling for AI Foundry - open the view in the extension if installed + if (this.resourceType === AzExtResourceType.AiFoundry && extension.isInstalled()) { + return [ + new GenericItem( + localize('openInFoundryExtension', 'Open in AI Foundry Extension'), + { + commandArgs: [], + commandId: 'microsoft-foundry-resources.focus', + contextValue: 'openInFoundryExtension', + iconPath: new vscode.ThemeIcon('link-external'), + id: `${this.id}/openInFoundryExtension` + }) + ]; + } + // If the extension is not installed and is not private, show an "Install extension" item - if (extension && !extension.isInstalled() && !extension.isPrivate()) { + if (!extension.isInstalled()) { return [ new GenericItem( localize('openInExtension', 'Open in {0} Extension', extension.label), diff --git a/test/grouping.test.ts b/test/grouping.test.ts index d5533720..c0742454 100644 --- a/test/grouping.test.ts +++ b/test/grouping.test.ts @@ -56,7 +56,7 @@ suite('Azure resource grouping tests', async () => { assert.ok(locationGroup); }); - test('Resource type group with no resources shows install extension item', async () => { + test('Resource type group with no resources shows install or open extension item', async () => { createMockSubscriptionWithFunctions(); await commands.executeCommand('azureResourceGroups.groupBy.resourceType'); @@ -75,10 +75,10 @@ suite('Azure resource grouping tests', async () => { if (aiFoundryGroup) { const children = await aiFoundryGroup.getChildren(); - // Should have at least one child (the "Install extension" item) - assert.ok(children && children.length > 0, 'Expected install extension item for empty resource type group'); + // Should have at least one child (either "Install extension" or "Open in AI Foundry Extension" item) + assert.ok(children && children.length > 0, 'Expected extension item for empty resource type group'); - // First child should be a GenericItem with the correct command + // First child should be a GenericItem const firstChild = children[0]; assert.ok(firstChild instanceof GenericItem, 'Expected first child to be a GenericItem'); } From de2601033ccc4ea1f45fc973958799f2b4e53cf6 Mon Sep 17 00:00:00 2001 From: alexweininger Date: Thu, 20 Nov 2025 14:24:06 -0500 Subject: [PATCH 4/6] Update icon --- src/tree/azure/grouping/ResourceTypeGroupingItem.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts index 991cdf32..e31af1d3 100644 --- a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts +++ b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import { AzExtResourceType } from "api/src/AzExtResourceType"; +import * as vscode from 'vscode'; import { getAzureExtensions } from "../../../AzExtWrapper"; import { canFocusContextValue } from "../../../constants"; import { localize } from "../../../utils/localize"; @@ -43,7 +43,7 @@ export class ResourceTypeGroupingItem extends GroupingItem { commandArgs: [], commandId: 'microsoft-foundry-resources.focus', contextValue: 'openInFoundryExtension', - iconPath: new vscode.ThemeIcon('link-external'), + iconPath: new vscode.ThemeIcon('symbol-method-arrow'), id: `${this.id}/openInFoundryExtension` }) ]; From 1fe5f6f438158846422e29e96fdb39c1c030e9e7 Mon Sep 17 00:00:00 2001 From: alexweininger Date: Thu, 20 Nov 2025 14:25:37 -0500 Subject: [PATCH 5/6] Cleanup extension name --- src/tree/azure/grouping/ResourceTypeGroupingItem.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts index e31af1d3..a384dc4a 100644 --- a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts +++ b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AzExtResourceType } from "api/src/AzExtResourceType"; +import { getName } from "src/utils/azureUtils"; import * as vscode from 'vscode'; import { getAzureExtensions } from "../../../AzExtWrapper"; import { canFocusContextValue } from "../../../constants"; @@ -38,7 +39,7 @@ export class ResourceTypeGroupingItem extends GroupingItem { if (this.resourceType === AzExtResourceType.AiFoundry && extension.isInstalled()) { return [ new GenericItem( - localize('openInFoundryExtension', 'Open in AI Foundry Extension'), + localize('openInFoundryExtension', `Open in ${getName(AzExtResourceType.AiFoundry)} Extension`), { commandArgs: [], commandId: 'microsoft-foundry-resources.focus', From f3731ee14b73ae8b0b3a99c6fe5544accaa81ad4 Mon Sep 17 00:00:00 2001 From: alexweininger Date: Thu, 20 Nov 2025 14:29:13 -0500 Subject: [PATCH 6/6] Fix test --- test/grouping.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/grouping.test.ts b/test/grouping.test.ts index c0742454..8a1fd59c 100644 --- a/test/grouping.test.ts +++ b/test/grouping.test.ts @@ -5,7 +5,6 @@ import { GroupingItem } from "../src/tree/azure/grouping/GroupingItem"; import { isLocationGroupingItem, LocationGroupingItem } from "../src/tree/azure/grouping/LocationGroupingItem"; import { isResourceGroupGroupingItem, ResourceGroupGroupingItem } from "../src/tree/azure/grouping/ResourceGroupGroupingItem"; import { isResourceTypeGroupingItem, ResourceTypeGroupingItem } from "../src/tree/azure/grouping/ResourceTypeGroupingItem"; -import { GenericItem } from "../src/tree/GenericItem"; import { createMockSubscriptionWithFunctions } from "./api/mockServiceFactory"; import { getCachedTestApi } from "./utils/testApiAccess"; @@ -65,22 +64,22 @@ suite('Azure resource grouping tests', async () => { const subscriptions = await tdp.getChildren(); const groups = await tdp.getChildren(subscriptions![0]) as GroupingItem[]; - + // Find a resource type group that has no resources (e.g., AiFoundry) - const aiFoundryGroup = groups.find(group => - isResourceTypeGroupingItem(group) && + const aiFoundryGroup = groups.find(group => + isResourceTypeGroupingItem(group) && (group as ResourceTypeGroupingItem).resourceType === AzExtResourceType.AiFoundry ); - + if (aiFoundryGroup) { const children = await aiFoundryGroup.getChildren(); - + // Should have at least one child (either "Install extension" or "Open in AI Foundry Extension" item) assert.ok(children && children.length > 0, 'Expected extension item for empty resource type group'); - + // First child should be a GenericItem const firstChild = children[0]; - assert.ok(firstChild instanceof GenericItem, 'Expected first child to be a GenericItem'); + assert.ok(firstChild && typeof firstChild === 'object' && 'label' in firstChild && 'id' in firstChild, 'Expected first child to be a GenericItem'); } }); });