diff --git a/package.json b/package.json index 441f4476e..148508066 100644 --- a/package.json +++ b/package.json @@ -277,6 +277,12 @@ "command": "containerApps.walkthrough.cleanUpResources", "title": "%containerApps.walkthrough.cleanUpResources%", "category": "Azure Container Apps" + }, + { + "command": "containerapps.toggleEnvironmentVariableVisibility", + "title": "%containerapps.toggleEnvironmentVariableVisibility%", + "category": "Azure Container Apps", + "icon": "$(eye)" } ], "submenus": [ @@ -526,6 +532,11 @@ "command": "containerApps.disconnectRepo", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /actionsConnected:true(.*)containerAppsActionsItem/i", "group": "1@2" + }, + { + "command": "containerapps.toggleEnvironmentVariableVisibility", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /environmentVariableItem/i", + "group": "inline" } ], "commandPalette": [ diff --git a/package.nls.json b/package.nls.json index 69a27ec60..31b09d08c 100644 --- a/package.nls.json +++ b/package.nls.json @@ -61,6 +61,7 @@ "containerApps.deploymentConfiguration.containerApp": "The name of the target container app.", "containerApps.deploymentConfiguration.containerRegistry": "The name of the container registry to use for storing and building images.", "containerApps.deploymentConfigurations": "A list of saved deployment configurations used for quickly redeploying a workspace project to a container app.", + "containerApp.toggleEnvironmentVariableVisibility": "Toggle Environment Variable Visibility.", "containerApps.walkthrough.gettingStarted.internal": "Open Walkthrough", "containerApps.walkthrough.gettingStarted.title": "Getting Started with Azure Container Apps", "containerApps.walkthrough.gettingStarted.description": "Learn to deploy a containerized application to Azure Container Apps using a sample GitHub repository.", diff --git a/resources/containers.svg b/resources/containers.svg new file mode 100644 index 000000000..fd6a1efcf --- /dev/null +++ b/resources/containers.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 8afc2e01f..3f9331a56 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerCommand, registerCommandWithTreeNodeUnwrapping, registerErrorHandler, registerReportIssueCommand } from '@microsoft/vscode-azext-utils'; +import { registerCommand, registerCommandWithTreeNodeUnwrapping, registerErrorHandler, registerReportIssueCommand, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type EnvironmentVariableItem } from '../tree/containers/EnvironmentVariableItem'; import { browseContainerAppNode } from './browseContainerApp'; import { createContainerApp } from './createContainerApp/createContainerApp'; import { createManagedEnvironment } from './createManagedEnvironment/createManagedEnvironment'; @@ -58,6 +59,10 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping('containerApps.editContainerApp', editContainerApp); registerCommandWithTreeNodeUnwrapping('containerApps.openConsoleInPortal', openConsoleInPortal); registerCommandWithTreeNodeUnwrapping('containerApps.updateImage', updateImage); + registerCommandWithTreeNodeUnwrapping('containerapps.toggleEnvironmentVariableVisibility', + async (context: IActionContext, item: EnvironmentVariableItem) => { + await item.toggleValueVisibility(context); + }); // deploy registerCommandWithTreeNodeUnwrapping('containerApps.deployImageApi', deployImageApi); diff --git a/src/tree/containers/ContainerItem.ts b/src/tree/containers/ContainerItem.ts new file mode 100644 index 000000000..3a539e0de --- /dev/null +++ b/src/tree/containers/ContainerItem.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { type Container, type Revision } from "@azure/arm-appcontainers"; +import { nonNullProp, nonNullValue, type TreeElementBase } from "@microsoft/vscode-azext-utils"; +import { type AzureSubscription, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; +import { TreeItemCollapsibleState, type TreeItem } from "vscode"; +import { getParentResource } from "../../utils/revisionDraftUtils"; +import { type ContainerAppModel } from "../ContainerAppItem"; +import { type RevisionsItemModel } from "../revisionManagement/RevisionItem"; +import { EnvironmentVariablesItem } from "./EnvironmentVariablesItem"; +import { ImageItem } from "./ImageItem"; + +export class ContainerItem implements RevisionsItemModel { + id: string; + label: string; + static readonly contextValue: string = 'containerItem'; + static readonly contextValueRegExp: RegExp = new RegExp(ContainerItem.contextValue); + + constructor(readonly subscription: AzureSubscription, readonly containerApp: ContainerAppModel, readonly revision: Revision, readonly container: Container) { + this.id = `${this.parentResource.id}/${container.name}`; + this.label = nonNullValue(this.container.name); + } + + getTreeItem(): TreeItem { + return { + id: this.id, + label: `${this.container.name}`, + contextValue: ContainerItem.contextValue, + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } + + getChildren(): TreeElementBase[] { + return [ + new ImageItem(this.subscription, this.containerApp, this.revision, this.id, this.container), + new EnvironmentVariablesItem(this.subscription, this.containerApp, this.revision, this.id, this.container) + ]; + } + + private get parentResource(): ContainerAppModel | Revision { + return getParentResource(this.containerApp, this.revision); + } + + viewProperties: ViewPropertiesModel = { + data: this.container, + label: nonNullProp(this.container, 'name'), + } +} diff --git a/src/tree/containers/ContainersItem.ts b/src/tree/containers/ContainersItem.ts new file mode 100644 index 000000000..0bdbd16f3 --- /dev/null +++ b/src/tree/containers/ContainersItem.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { KnownActiveRevisionsMode, type Container, type Revision } from "@azure/arm-appcontainers"; +import { nonNullValue, nonNullValueAndProp, type TreeElementBase } from "@microsoft/vscode-azext-utils"; +import { type AzureSubscription, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; +import * as deepEqual from 'deep-eql'; +import { TreeItemCollapsibleState, type TreeItem } from "vscode"; +import { ext } from "../../extensionVariables"; +import { localize } from "../../utils/localize"; +import { getParentResource } from "../../utils/revisionDraftUtils"; +import { treeUtils } from "../../utils/treeUtils"; +import { type ContainerAppModel } from "../ContainerAppItem"; +import { RevisionDraftDescendantBase } from "../revisionManagement/RevisionDraftDescendantBase"; +import { RevisionDraftItem } from "../revisionManagement/RevisionDraftItem"; +import { ContainerItem } from "./ContainerItem"; +import { EnvironmentVariablesItem } from "./EnvironmentVariablesItem"; +import { ImageItem } from "./ImageItem"; + +export class ContainersItem extends RevisionDraftDescendantBase { + id: string; + label: string; + private containers: Container[] = []; + + constructor(public readonly subscription: AzureSubscription, + public readonly containerApp: ContainerAppModel, + public readonly revision: Revision,) { + super(subscription, containerApp, revision); + this.id = `${this.parentResource.id}/containers`; + this.containers = nonNullValue(revision.template?.containers); + this.label = this.containers.length === 1 ? localize('container', 'Container') : localize('containers', 'Containers'); + } + + getChildren(): TreeElementBase[] { + if (this.containers.length === 1) { + return [new ImageItem(this.subscription, this.containerApp, this.revision, this.id, this.containers[0]), + new EnvironmentVariablesItem(this.subscription, this.containerApp, this.revision, this.id, this.containers[0])]; + } + return nonNullValue(this.containers?.map(container => new ContainerItem(this.subscription, this.containerApp, this.revision, container))); + } + + getTreeItem(): TreeItem { + return { + id: this.id, + label: this.label, + iconPath: treeUtils.getIconPath('containers'), + collapsibleState: TreeItemCollapsibleState.Collapsed + } + } + + private get parentResource(): ContainerAppModel | Revision { + return getParentResource(this.containerApp, this.revision); + } + + protected setProperties(): void { + this.label = this.containers.length === 1 ? localize('container', 'Container') : localize('containers', 'Containers'); + this.containers = nonNullValueAndProp(this.parentResource.template, 'containers'); + } + + protected setDraftProperties(): void { + this.label = this.containers.length === 1 ? localize('container*', 'Container*') : localize('containers*', 'Containers*'); + this.containers = nonNullValueAndProp(ext.revisionDraftFileSystem.parseRevisionDraft(this), 'containers'); + } + + viewProperties: ViewPropertiesModel = { + label: 'Containers', + getData: async () => { + return this.containers.length === 1 ? this.containers[0] : JSON.stringify(this.containers) + } + } + + hasUnsavedChanges(): boolean { + // We only care about showing changes to descendants of the revision draft item when in multiple revisions mode + if (this.containerApp.revisionsMode === KnownActiveRevisionsMode.Multiple && !RevisionDraftItem.hasDescendant(this)) { + return false; + } + + const draftTemplate = ext.revisionDraftFileSystem.parseRevisionDraft(this)?.containers; + const currentTemplate = this.parentResource.template?.containers; + + if (!draftTemplate) { + return false; + } + + return !deepEqual(currentTemplate, draftTemplate); + } +} diff --git a/src/tree/containers/EnvironmentVariableItem.ts b/src/tree/containers/EnvironmentVariableItem.ts new file mode 100644 index 000000000..9d0cc19ce --- /dev/null +++ b/src/tree/containers/EnvironmentVariableItem.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { type Container, type EnvironmentVar, type Revision } from "@azure/arm-appcontainers"; +import { type IActionContext } from "@microsoft/vscode-azext-utils"; +import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { ThemeIcon, type TreeItem } from "vscode"; +import { ext } from "../../extensionVariables"; +import { localize } from "../../utils/localize"; +import { getParentResource } from "../../utils/revisionDraftUtils"; +import { type ContainerAppModel } from "../ContainerAppItem"; +import { type RevisionsItemModel } from "../revisionManagement/RevisionItem"; + +export class EnvironmentVariableItem implements RevisionsItemModel { + _hideValue: boolean; + constructor(public readonly subscription: AzureSubscription, + public readonly containerApp: ContainerAppModel, + public readonly revision: Revision, + readonly containerId: string, + readonly container: Container, + readonly envVariable: EnvironmentVar) { + this._hideValue = true; + } + id: string = `${this.parentResource.id}/${this.container.image}/${this.envVariable.name}` + + getTreeItem(): TreeItem { + return { + label: this._hideValue ? `${this.envVariable.name}=Hidden value. Click to view.` : `${this.envVariable.name}=${this.envVariable.value}`, + contextValue: 'environmentVariableItem', + iconPath: new ThemeIcon('symbol-constant'), + command: { + command: 'containerapps.toggleEnvironmentVariableVisibility', + title: localize('commandtitle', 'Toggle Environment Variable Visibility'), + arguments: [this, this._hideValue,] + } + } + } + + public async toggleValueVisibility(_: IActionContext): Promise { + this._hideValue = !this._hideValue; + ext.branchDataProvider.refresh(this); + } + + private get parentResource(): ContainerAppModel | Revision { + return getParentResource(this.containerApp, this.revision); + } +} diff --git a/src/tree/containers/EnvironmentVariablesItem.ts b/src/tree/containers/EnvironmentVariablesItem.ts new file mode 100644 index 000000000..14c522e02 --- /dev/null +++ b/src/tree/containers/EnvironmentVariablesItem.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { type Container, type Revision } from "@azure/arm-appcontainers"; +import { type TreeElementBase } from "@microsoft/vscode-azext-utils"; +import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from "vscode"; +import { localize } from "../../utils/localize"; +import { getParentResource } from "../../utils/revisionDraftUtils"; +import { type ContainerAppModel } from "../ContainerAppItem"; +import { type RevisionsItemModel } from "../revisionManagement/RevisionItem"; +import { EnvironmentVariableItem } from "./EnvironmentVariableItem"; + +export class EnvironmentVariablesItem implements RevisionsItemModel { + static readonly contextValue: string = 'environmentVariablesItem'; + static readonly contextValueRegExp: RegExp = new RegExp(EnvironmentVariablesItem.contextValue); + + constructor(public readonly subscription: AzureSubscription, + public readonly containerApp: ContainerAppModel, + public readonly revision: Revision, + readonly containerId: string, + readonly container: Container) { + } + id: string = `${this.parentResource.id}/environmentVariables/${this.container.image}`; + + getTreeItem(): TreeItem { + return { + id: this.id, + label: localize('environmentVariables', 'Environment Variables'), + iconPath: new ThemeIcon('settings'), + contextValue: EnvironmentVariablesItem.contextValue, + collapsibleState: TreeItemCollapsibleState.Collapsed + } + } + + getChildren(): TreeElementBase[] | undefined { + if (!this.container.env) { + return; + } + return this.container.env?.map(env => new EnvironmentVariableItem(this.subscription, this.containerApp, this.revision, this.id, this.container, env)); + } + + private get parentResource(): ContainerAppModel | Revision { + return getParentResource(this.containerApp, this.revision); + } +} diff --git a/src/tree/containers/ImageItem.ts b/src/tree/containers/ImageItem.ts new file mode 100644 index 000000000..20b43dd9b --- /dev/null +++ b/src/tree/containers/ImageItem.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { type Container, type Revision } from "@azure/arm-appcontainers"; +import { createGenericElement, nonNullProp, nonNullValue, type TreeElementBase } from "@microsoft/vscode-azext-utils"; +import { type AzureSubscription, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; +import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from "vscode"; +import { localize } from "../../utils/localize"; +import { getParentResource } from "../../utils/revisionDraftUtils"; +import { type ContainerAppModel } from "../ContainerAppItem"; +import { type RevisionsItemModel } from "../revisionManagement/RevisionItem"; + +export class ImageItem implements RevisionsItemModel { + static readonly contextValue: string = 'imageItem'; + static readonly contextValueRegExp: RegExp = new RegExp(ImageItem.contextValue); + readonly loginServer = this.container.image?.split('/')[0]; + readonly imageAndTag = this.container.image?.substring(nonNullValue(this.loginServer?.length) + 1, this.container.image?.length); + + constructor( + readonly subscription: AzureSubscription, + readonly containerApp: ContainerAppModel, + readonly revision: Revision, + readonly containerId: string, + readonly container: Container) { } + id: string = `${this.parentResource.id}/image/${this.imageAndTag}` + + getTreeItem(): TreeItem { + return { + id: this.id, + label: localize('image', 'Image'), + iconPath: new ThemeIcon('window'), + contextValue: ImageItem.contextValue, + collapsibleState: TreeItemCollapsibleState.Collapsed, + } + } + + getChildren(): TreeElementBase[] { + return [ + createGenericElement({ + id: `${this.id}/imageName`, + label: localize('containerImage', 'Name:'), + contextValue: 'containerImageNameItem', + description: `${this.imageAndTag}`, + }), + createGenericElement({ + id: `${this.id}/imageRegistry`, + label: localize('containerImageRegistryItem', 'Registry:'), + contextValue: 'containerImageRegistryItem', + description: `${this.loginServer}`, + }) + ]; + } + + private get parentResource(): ContainerAppModel | Revision { + return getParentResource(this.containerApp, this.revision); + } + + viewProperties: ViewPropertiesModel = { + data: this.container, + label: nonNullProp(this.container, 'name'), + } +} diff --git a/src/tree/revisionManagement/RevisionItem.ts b/src/tree/revisionManagement/RevisionItem.ts index d7e673f8e..f49b88542 100644 --- a/src/tree/revisionManagement/RevisionItem.ts +++ b/src/tree/revisionManagement/RevisionItem.ts @@ -12,6 +12,7 @@ import { ext } from "../../extensionVariables"; import { localize } from "../../utils/localize"; import { type ContainerAppModel } from "../ContainerAppItem"; import { type ContainerAppsItem, type TreeElementBase } from "../ContainerAppsBranchDataProvider"; +import { ContainersItem } from "../containers/ContainersItem"; import { ScaleItem } from "../scaling/ScaleItem"; import { RevisionDraftDescendantBase } from "./RevisionDraftDescendantBase"; @@ -68,6 +69,7 @@ export class RevisionItem implements RevisionsItemModel { static getTemplateChildren(subscription: AzureSubscription, containerApp: ContainerAppModel, revision: Revision): TreeElementBase[] { return [ + RevisionDraftDescendantBase.createTreeItem(ContainersItem, subscription, containerApp, revision), RevisionDraftDescendantBase.createTreeItem(ScaleItem, subscription, containerApp, revision) ]; }