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..a384dc4a 100644 --- a/src/tree/azure/grouping/ResourceTypeGroupingItem.ts +++ b/src/tree/azure/grouping/ResourceTypeGroupingItem.ts @@ -4,7 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 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"; +import { localize } from "../../../utils/localize"; +import { GenericItem } from "../../GenericItem"; +import { ResourceGroupsItem } from "../../ResourceGroupsItem"; import { GroupingItem, GroupingItemOptions } from "./GroupingItem"; import { GroupingItemFactoryOptions } from "./GroupingItemFactory"; @@ -18,6 +24,49 @@ 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 (!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 ${getName(AzExtResourceType.AiFoundry)} Extension`), + { + commandArgs: [], + commandId: 'microsoft-foundry-resources.focus', + contextValue: 'openInFoundryExtension', + iconPath: new vscode.ThemeIcon('symbol-method-arrow'), + id: `${this.id}/openInFoundryExtension` + }) + ]; + } + + // If the extension is not installed and is not private, show an "Install extension" item + if (!extension.isInstalled()) { + 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..8a1fd59c 100644 --- a/test/grouping.test.ts +++ b/test/grouping.test.ts @@ -54,4 +54,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 or open 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 (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 && typeof firstChild === 'object' && 'label' in firstChild && 'id' in firstChild, 'Expected first child to be a GenericItem'); + } + }); });