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

v2 edit tags command for resources #461

Merged
merged 6 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
90 changes: 59 additions & 31 deletions src/commands/tags/TagFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', '<Insert tag name>');
const insertValueHere: string = localize('insertTagValue', '<Insert tag value>');

export interface ITagsModel extends AzExtTreeFileSystemItem {
getTags(): Promise<Tags>;
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<Tags> {
return await callWithTelemetryAndErrorHandling('getTags', async (context): Promise<Tags | undefined> => {
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<ResourceGroupTreeItem | AppResourceTreeItem> {
export class TagFileSystem extends AzExtTreeFileSystem<ITagsModel> {
public static scheme: string = 'azureResourceGroups';
public scheme: string = TagFileSystem.scheme;

public async statImpl(_context: IActionContext, node: ResourceGroupTreeItem | AppResourceTreeItem): Promise<FileStat> {
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<FileStat> {
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<Uint8Array> {
public async readFileImpl(_context: IActionContext, node: ITagsModel): Promise<Uint8Array> {
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<void> {
public async writeFileImpl(context: IActionContext, model: ITagsModel, content: Uint8Array, originalUri: Uri): Promise<void> {
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');
Expand All @@ -64,9 +94,7 @@ export class TagFileSystem extends AzExtTreeFileSystem<ResourceGroupTreeItem | A
// This won't be displayed, but might as well track the first diagnostic for telemetry
throw new Error(diagnostics[0].message);
} else {
const confirmMessage: string = isResourceGroup ?
localize('confirmTagsGroup', 'Are you sure you want to update tags for resource group "{0}"?', node.name) :
localize('confirmTagsResource', 'Are you sure you want to update tags for resource "{0}"?', node.name);
const confirmMessage: string = localize('confirmTags', 'Are you sure you want to update tags for {0}?', this.getDetailedName(model));
const update: MessageItem = { title: localize('update', 'Update') };
await context.ui.showWarningMessage(confirmMessage, { modal: true }, update);

Expand All @@ -77,19 +105,18 @@ export class TagFileSystem extends AzExtTreeFileSystem<ResourceGroupTreeItem | A
delete tags[insertKeyHere];
}

const client: ResourceManagementClient = await createResourceClient([context, node]);
await client.tagsOperations.updateAtScope(node.id, { properties: { tags }, operation: 'Replace' });
const subscriptionContext = createSubscriptionContext(model.subscription);
const client: ResourceManagementClient = await createResourceClient([context, subscriptionContext]);
await client.tagsOperations.updateAtScope(model.id, { properties: { tags }, operation: 'Replace' });

const updatedMessage: string = isResourceGroup ?
localize('updatedTagsGroup', 'Successfully updated tags for resource group "{0}".', node.name) :
localize('updatedTagsResource', 'Successfully updated tags for resource "{0}".', node.name);
const updatedMessage: string = localize('updatedTags', 'Successfully updated tags for {0}.', this.getDetailedName(model));
void window.showInformationMessage(updatedMessage);
ext.outputChannel.appendLog(updatedMessage);
}
}

public getFilePath(node: ResourceGroupTreeItem | AppResourceTreeItem): string {
return `${node.name}-tags.jsonc`;
public getFilePath(node: ITagsModel): string {
return `${node.displayName}-tags.jsonc`;
}

private getFileContentFromTags(tags: {} | undefined): string {
Expand All @@ -105,10 +132,11 @@ export class TagFileSystem extends AzExtTreeFileSystem<ResourceGroupTreeItem | A
return `// ${comment}${os.EOL}${JSON.stringify(tags, undefined, 4)}`;
}

private async getTagsFromNode(node: ResourceGroupTreeItem | AppResourceTreeItem): Promise<Tags | undefined> {
if (node instanceof ResourceGroupTreeItem) {
return (await node.getData())?.tags;
}
return node.data.tags;
private async getTagsFromNode(node: ITagsModel): Promise<Tags | undefined> {
return await node.getTags();
}

private getDetailedName(node: ITagsModel): string {
return `${node.displayType} "${node.displayName}"`;
alexweininger marked this conversation as resolved.
Show resolved Hide resolved
}
}
18 changes: 12 additions & 6 deletions src/commands/tags/editTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
if (!node) {
node = await pickAppResource<AppResourceTreeItem>(context);
export async function editTags(_context: IActionContext, item?: AzureResourceItem<AzureResource>): Promise<void> {
if (!item) {
// todo
// node = await pickAppResource<AppResourceTreeItem>(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);
}
5 changes: 2 additions & 3 deletions src/tree/AppResourceTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 });
Copy link
Member

Choose a reason for hiding this comment

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

Could you explain why you commented this out? Should it go back eventually?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a v1 class that isn't used anymore. It's a lot of work to delete everything, will do in a follow up PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

When I start deleting files it just shows how much there is left to do. For example:

  1. Toggle show all resources command
  2. Group by tags needs updated so that it doesn't rely on the root tree item to get subscriptions
  3. revealTreeItem

I'm sure there's more


this.type = resource.type;
this.kind = resource.kind;
Expand Down Expand Up @@ -102,7 +101,7 @@ export class AppResourceTreeItem extends ResolvableTreeItemBase implements Group

public async refreshImpl(context: IActionContext): Promise<void> {
this.mTime = Date.now();
ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this });
// ext.tagFS.fireSoon({ type: FileChangeType.Changed, item: this });
await super.refreshImpl(context);
}

Expand Down
6 changes: 2 additions & 4 deletions src/tree/ResourceGroupTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand Down
15 changes: 10 additions & 5 deletions src/tree/v2/azure/AzureResourceItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends ResourceBase> extends BranchDataProviderItem {
export class AzureResourceItem<T extends AzureResource> extends BranchDataProviderItem {
constructor(
public readonly resource: T,
branchItem: ResourceModelBase,
Expand All @@ -18,9 +20,12 @@ export class AzureResourceItem<T extends ResourceBase> 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<ResourceGroupsItem | undefined> {
return this.parent;
Expand All @@ -33,8 +38,8 @@ export class AzureResourceItem<T extends ResourceBase> extends BranchDataProvide
}
}

export type ResourceItemFactory<T extends ResourceBase> = (resource: T, branchItem: ResourceModelBase, branchDataProvider: BranchDataProvider<ResourceBase, ResourceModelBase>, parent?: ResourceGroupsItem, options?: BranchDataItemOptions) => AzureResourceItem<T>;
export type ResourceItemFactory<T extends AzureResource> = (resource: T, branchItem: ResourceModelBase, branchDataProvider: BranchDataProvider<ResourceBase, ResourceModelBase>, parent?: ResourceGroupsItem, options?: BranchDataItemOptions) => AzureResourceItem<T>;

export function createResourceItemFactory<T extends ResourceBase>(itemCache: BranchDataItemCache): ResourceItemFactory<T> {
export function createResourceItemFactory<T extends AzureResource>(itemCache: BranchDataItemCache): ResourceItemFactory<T> {
return (resource, branchItem, branchDataProvider, parent, options) => new AzureResourceItem(resource, branchItem, branchDataProvider, itemCache, parent, options);
}