Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/AzExtWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<apiUtils.AzureExtensionApiProvider> | undefined {
return extensions.getExtension(this.id);
}
Expand Down
13 changes: 13 additions & 0 deletions src/tree/azure/grouping/GroupingItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResourceGroupsItem[] | undefined> {

const sortedResources = this.getResourcesToDisplay(this.resources).sort((a, b) => {
Expand All @@ -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<AzureSubscription, AzureResource[]>();

sortedResources.forEach(resource => {
Expand Down
49 changes: 49 additions & 0 deletions src/tree/azure/grouping/ResourceTypeGroupingItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions test/grouping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});