diff --git a/package-lock.json b/package-lock.json index d2913baa..82b56043 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@azure/arm-resources": "5.0.0", "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", "@microsoft/vscode-azext-azureutils": "^0.3.4", - "@microsoft/vscode-azext-utils": "^0.3.20", + "@microsoft/vscode-azext-utils": "^0.3.21", "jsonc-parser": "^2.2.1", "open": "^8.0.4", "semver": "^7.3.7", @@ -806,9 +806,9 @@ } }, "node_modules/@microsoft/vscode-azext-utils": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.3.20.tgz", - "integrity": "sha512-8Gs6bIjMamZ2BoUsqLGGRgQfKvwmPA9PB7CUtPab9/72lPN9ugGxoSAyYdXWnmZefJ1NBwyyf0sfSq7ggz1M+Q==", + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.3.21.tgz", + "integrity": "sha512-E4kMgvbR/oUZ6c4SCb5QsCrsa5SVKB4WMHyPH5SrUZ3h9eMVTGBM7AzG1n663FSI0vb7/RanaaOVmXv/asZDzQ==", "dependencies": { "@vscode/extension-telemetry": "^0.6.2", "dayjs": "^1.11.2", @@ -12699,9 +12699,9 @@ } }, "@microsoft/vscode-azext-utils": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.3.20.tgz", - "integrity": "sha512-8Gs6bIjMamZ2BoUsqLGGRgQfKvwmPA9PB7CUtPab9/72lPN9ugGxoSAyYdXWnmZefJ1NBwyyf0sfSq7ggz1M+Q==", + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-0.3.21.tgz", + "integrity": "sha512-E4kMgvbR/oUZ6c4SCb5QsCrsa5SVKB4WMHyPH5SrUZ3h9eMVTGBM7AzG1n663FSI0vb7/RanaaOVmXv/asZDzQ==", "requires": { "@vscode/extension-telemetry": "^0.6.2", "dayjs": "^1.11.2", diff --git a/package.json b/package.json index 264894db..752ad1e3 100644 --- a/package.json +++ b/package.json @@ -590,7 +590,7 @@ "@azure/arm-resources": "5.0.0", "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", "@microsoft/vscode-azext-azureutils": "^0.3.4", - "@microsoft/vscode-azext-utils": "^0.3.20", + "@microsoft/vscode-azext-utils": "^0.3.21", "jsonc-parser": "^2.2.1", "open": "^8.0.4", "semver": "^7.3.7", diff --git a/src/commands/tags/TagFileSystem.ts b/src/commands/tags/TagFileSystem.ts index 3ef33b84..c7b9bd01 100644 --- a/src/commands/tags/TagFileSystem.ts +++ b/src/commands/tags/TagFileSystem.ts @@ -4,54 +4,84 @@ *--------------------------------------------------------------------------------------------*/ import { ResourceManagementClient, Tags } from "@azure/arm-resources"; -import { AzExtTreeFileSystem, IActionContext } from '@microsoft/vscode-azext-utils'; +import { uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { AzExtTreeFileSystem, AzExtTreeFileSystemItem, callWithTelemetryAndErrorHandling, IActionContext, nonNullValue } from '@microsoft/vscode-azext-utils'; +import { AzureResource, AzureSubscription } from "@microsoft/vscode-azext-utils/hostapi.v2"; import * as jsonc from 'jsonc-parser'; import * as os from "os"; import { commands, Diagnostic, DiagnosticSeverity, FileStat, FileType, languages, MessageItem, Uri, window } from "vscode"; import { ext } from "../../extensionVariables"; -import { AppResourceTreeItem } from "../../tree/AppResourceTreeItem"; -import { ResourceGroupTreeItem } from "../../tree/ResourceGroupTreeItem"; import { createResourceClient } from "../../utils/azureClients"; import { localize } from "../../utils/localize"; +import { createSubscriptionContext } from "../../utils/v2/credentialsUtils"; const insertKeyHere: string = localize('insertTagName', ''); const insertValueHere: string = localize('insertTagValue', ''); +export interface ITagsModel extends AzExtTreeFileSystemItem { + getTags(): Promise; + subscription: AzureSubscription; + displayName: string; + displayType: 'resource group' | 'resource'; + + cTime: number; + mTime: number; +} + +export class ResourceTags implements ITagsModel { + constructor(private readonly resource: AzureResource) { } + + readonly id: string = this.resource.id; + readonly subscription: AzureSubscription = this.resource.subscription; + + readonly displayName: string = this.resource.name; + readonly displayType: ITagsModel['displayType'] = 'resource'; + + cTime: number; + mTime: number; + + async getTags(): Promise { + return await callWithTelemetryAndErrorHandling('getTags', async (context): Promise => { + const subscriptionContext = createSubscriptionContext(this.resource.subscription); + const client = await createResourceClient([context, subscriptionContext]); + // use list because getById is only available for certain api versions and locations + const resources = await uiUtils.listAllIterator(client.resources.listByResourceGroup(nonNullValue(this.resource.resourceGroup))); + return resources.find(r => r.id === this.id)?.tags; + }) ?? {}; + } +} + /** - * For now this file system only supports editing tags. - * However, the scheme was left generic so that it could support editing other stuff in this extension without needing to create a whole new file system + * File system for editing resource tags. */ -export class TagFileSystem extends AzExtTreeFileSystem { +export class TagFileSystem extends AzExtTreeFileSystem { public static scheme: string = 'azureResourceGroups'; public scheme: string = TagFileSystem.scheme; - public async statImpl(_context: IActionContext, node: ResourceGroupTreeItem | AppResourceTreeItem): Promise { - const fileContent: string = this.getFileContentFromTags(await this.getTagsFromNode(node)); - return { type: FileType.File, ctime: node.cTime, mtime: node.mTime, size: Buffer.byteLength(fileContent) }; + public async statImpl(_context: IActionContext, model: ITagsModel): Promise { + const fileContent: string = this.getFileContentFromTags(await this.getTagsFromNode(model)); + return { type: FileType.File, ctime: model.cTime, mtime: model.mTime, size: Buffer.byteLength(fileContent) }; } - public async readFileImpl(_context: IActionContext, node: ResourceGroupTreeItem | AppResourceTreeItem): Promise { + public async readFileImpl(_context: IActionContext, node: ITagsModel): Promise { const fileContent: string = this.getFileContentFromTags(await this.getTagsFromNode(node)); return Buffer.from(fileContent); } - public async writeFileImpl(context: IActionContext, node: ResourceGroupTreeItem | AppResourceTreeItem, content: Uint8Array, originalUri: Uri): Promise { + public async writeFileImpl(context: IActionContext, model: ITagsModel, content: Uint8Array, originalUri: Uri): Promise { const text: string = content.toString(); - const isResourceGroup: boolean = node instanceof ResourceGroupTreeItem; const diagnostics: Diagnostic[] = languages.getDiagnostics(originalUri).filter(d => d.severity === DiagnosticSeverity.Error); if (diagnostics.length > 0) { context.telemetry.measurements.tagDiagnosticsLength = diagnostics.length; const showErrors: MessageItem = { title: localize('showErrors', 'Show Errors') }; - const message: string = isResourceGroup ? - localize('errorsExistGroup', 'Failed to upload tags for resource group "{0}".', node.name) : - localize('errorsExistResource', 'Failed to upload tags for resource "{0}".', node.name); + const message: string = localize('errorsExist', 'Failed to upload tags for {0}.', this.getDetailedName(model)); void window.showErrorMessage(message, showErrors).then(async (result) => { if (result === showErrors) { const openedUri: Uri | undefined = window.activeTextEditor?.document.uri; if (!openedUri || originalUri.query !== openedUri.query) { - await this.showTextDocument(node); + await this.showTextDocument(model); } await commands.executeCommand('workbench.action.showErrorsWarnings'); @@ -64,9 +94,7 @@ export class TagFileSystem extends AzExtTreeFileSystem { - if (node instanceof ResourceGroupTreeItem) { - return (await node.getData())?.tags; - } - return node.data.tags; + private async getTagsFromNode(node: ITagsModel): Promise { + return await node.getTags(); + } + + private getDetailedName(node: ITagsModel): string { + return `${node.displayType} "${node.displayName}"`; } } diff --git a/src/commands/tags/editTags.ts b/src/commands/tags/editTags.ts index 94637bf9..beb21a7f 100644 --- a/src/commands/tags/editTags.ts +++ b/src/commands/tags/editTags.ts @@ -4,14 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from "@microsoft/vscode-azext-utils"; -import { pickAppResource } from "../../api/pickAppResource"; +import { AzureResource } from "@microsoft/vscode-azext-utils/hostapi.v2"; import { ext } from "../../extensionVariables"; -import { AppResourceTreeItem } from "../../tree/AppResourceTreeItem"; +import { AzureResourceItem } from "../../tree/v2/azure/AzureResourceItem"; -export async function editTags(context: IActionContext, node?: AppResourceTreeItem): Promise { - if (!node) { - node = await pickAppResource(context); +export async function editTags(_context: IActionContext, item?: AzureResourceItem): Promise { + if (!item) { + // todo + // node = await pickAppResource(context); + throw new Error("A resource must be selected."); } - await ext.tagFS.showTextDocument(node); + if (!item.tagsModel) { + throw new Error("Editing tags is not supported for this resource."); + } + + await ext.tagFS.showTextDocument(item.tagsModel); } diff --git a/src/tree/AppResourceTreeItem.ts b/src/tree/AppResourceTreeItem.ts index 216c360f..6f2a9430 100644 --- a/src/tree/AppResourceTreeItem.ts +++ b/src/tree/AppResourceTreeItem.ts @@ -6,7 +6,6 @@ import { ResourceGroup } from "@azure/arm-resources"; import { AzExtParentTreeItem, AzExtResourceType, AzExtTreeItem, IActionContext, nonNullProp, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; import { AppResource, GroupableResource, GroupingConfig, GroupNodeConfiguration, ResolvedAppResourceBase } from "@microsoft/vscode-azext-utils/hostapi"; -import { FileChangeType } from "vscode"; import { azureExtensions } from "../azureExtensions"; import { GroupBySettings } from "../commands/explorer/groupBy"; import { ungroupedId } from "../constants"; @@ -46,7 +45,7 @@ export class AppResourceTreeItem extends ResolvableTreeItemBase implements Group this.groupConfig = createGroupConfigFromResource(resource, root.id); this.contextValues.add(AppResourceTreeItem.contextValue); - ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); + // ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); this.type = resource.type; this.kind = resource.kind; @@ -102,7 +101,7 @@ export class AppResourceTreeItem extends ResolvableTreeItemBase implements Group public async refreshImpl(context: IActionContext): Promise { this.mTime = Date.now(); - ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); + // ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); await super.refreshImpl(context); } diff --git a/src/tree/ResourceGroupTreeItem.ts b/src/tree/ResourceGroupTreeItem.ts index 5df0e276..a1f45c1b 100644 --- a/src/tree/ResourceGroupTreeItem.ts +++ b/src/tree/ResourceGroupTreeItem.ts @@ -6,10 +6,8 @@ import { ResourceGroup, ResourceManagementClient } from "@azure/arm-resources"; import { AzExtParentTreeItem, AzureWizard, IActionContext, nonNullProp, TreeItemIconPath } from "@microsoft/vscode-azext-utils"; import { GroupNodeConfiguration } from "@microsoft/vscode-azext-utils/hostapi"; -import { FileChangeType } from "vscode"; import { DeleteResourceGroupContext } from "../commands/deleteResourceGroup/DeleteResourceGroupContext"; import { DeleteResourceGroupStep } from "../commands/deleteResourceGroup/DeleteResourceGroupStep"; -import { ext } from "../extensionVariables"; import { createActivityContext } from "../utils/activityUtils"; import { createResourceClient } from "../utils/azureClients"; import { localize } from "../utils/localize"; @@ -34,7 +32,7 @@ export class ResourceGroupTreeItem extends GroupTreeItemBase { this.data = rg; }); - ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); + // ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); } public static createFromResourceGroup(parent: AzExtParentTreeItem, rg: ResourceGroup): ResourceGroupTreeItem { @@ -67,7 +65,7 @@ export class ResourceGroupTreeItem extends GroupTreeItemBase { const client: ResourceManagementClient = await createResourceClient([context, this.subscription]); this.data = await client.resourceGroups.get(this.name); this.mTime = Date.now(); - ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); + // ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this }); await super.refreshImpl(context); } diff --git a/src/tree/v2/azure/AzureResourceItem.ts b/src/tree/v2/azure/AzureResourceItem.ts index add1c887..554b578f 100644 --- a/src/tree/v2/azure/AzureResourceItem.ts +++ b/src/tree/v2/azure/AzureResourceItem.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BranchDataProvider, ResourceBase, ResourceModelBase } from '@microsoft/vscode-azext-utils/hostapi.v2'; -import { TreeItem } from 'vscode'; +import { AzureResource, BranchDataProvider, ResourceBase, ResourceModelBase } from '@microsoft/vscode-azext-utils/hostapi.v2'; +import { FileChangeType, TreeItem } from 'vscode'; +import { ResourceTags } from '../../../commands/tags/TagFileSystem'; +import { ext } from '../../../extensionVariables'; import { BranchDataItemCache } from '../BranchDataItemCache'; import { BranchDataItemOptions, BranchDataProviderItem } from '../BranchDataProviderItem'; import { ResourceGroupsItem } from '../ResourceGroupsItem'; -export class AzureResourceItem extends BranchDataProviderItem { +export class AzureResourceItem extends BranchDataProviderItem { constructor( public readonly resource: T, branchItem: ResourceModelBase, @@ -18,9 +20,12 @@ export class AzureResourceItem extends BranchDataProvide private readonly parent?: ResourceGroupsItem, options?: BranchDataItemOptions) { super(branchItem, branchDataProvider, itemCache, options); + + ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this.tagsModel }); } readonly id = this.resource.id; + readonly tagsModel = new ResourceTags(this.resource); override async getParent(): Promise { return this.parent; @@ -33,8 +38,8 @@ export class AzureResourceItem extends BranchDataProvide } } -export type ResourceItemFactory = (resource: T, branchItem: ResourceModelBase, branchDataProvider: BranchDataProvider, parent?: ResourceGroupsItem, options?: BranchDataItemOptions) => AzureResourceItem; +export type ResourceItemFactory = (resource: T, branchItem: ResourceModelBase, branchDataProvider: BranchDataProvider, parent?: ResourceGroupsItem, options?: BranchDataItemOptions) => AzureResourceItem; -export function createResourceItemFactory(itemCache: BranchDataItemCache): ResourceItemFactory { +export function createResourceItemFactory(itemCache: BranchDataItemCache): ResourceItemFactory { return (resource, branchItem, branchDataProvider, parent, options) => new AzureResourceItem(resource, branchItem, branchDataProvider, itemCache, parent, options); }