diff --git a/package.json b/package.json index b837aea4d..053609bd2 100644 --- a/package.json +++ b/package.json @@ -80,11 +80,6 @@ "title": "%containerApps.editContainerApp%", "category": "Azure Container Apps" }, - { - "command": "containerApps.updateImage", - "title": "%containerApps.updateImage%", - "category": "Azure Container Apps" - }, { "command": "containerApps.deployImageApi", "title": "%containerApps.deployImageApi%", @@ -203,6 +198,11 @@ "title": "%containerApps.openConsoleInPortal%", "category": "Azure Container Apps" }, + { + "command": "containerApps.editContainer", + "title": "%containerApps.editContainer%", + "category": "Azure Container Apps" + }, { "command": "containerApps.editScaleRange", "title": "%containerApps.editScaleRange%", @@ -373,11 +373,6 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem(.*)revisionMode:single(.*)unsavedChanges:true/i", "group": "3@2" }, - { - "command": "containerApps.updateImage", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem(.*)revisionMode:single/i", - "group": "4@1" - }, { "command": "containerApps.editContainerApp", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem(.*)revisionMode:single/i", @@ -438,11 +433,6 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionItem(.*)revisionState:active/i", "group": "2@2" }, - { - "command": "containerApps.updateImage", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraft:false(.*)revisionItem/i", - "group": "3@1" - }, { "command": "containerApps.deployRevisionDraft", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem(.*)unsavedChanges:true/i", @@ -463,16 +453,16 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem/i", "group": "1@2" }, - { - "command": "containerApps.updateImage", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem/i", - "group": "2@1" - }, { "command": "containerApps.editRevisionDraft", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /revisionDraftItem/i", "group": "3@1" }, + { + "command": "containerApps.editContainer", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerItem/i", + "group": "1@1" + }, { "command": "containerApps.editScaleRange", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /scaleItem/i", diff --git a/package.nls.json b/package.nls.json index b7b548693..ebde104d0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -9,7 +9,7 @@ "containerApps.createContainerApp": "Create Container App...", "containerApps.createContainerAppFromWorkspace": "Create Container App from Workspace...", "containerApps.editContainerApp": "Edit Container App (Advanced)...", - "containerApps.updateImage": "Update Container Image...", + "containerApps.editContainer": "Edit Container...", "containerApps.deployImageApi": "Deploy Image to Container App (API)...", "containerApps.deployWorkspaceProject": "Deploy Project from Workspace...", "containerApps.deployWorkspaceProjectApi": "Deploy Project from Workspace (API)...", diff --git a/src/commands/editContainer/ContainerEditContext.ts b/src/commands/editContainer/ContainerEditContext.ts new file mode 100644 index 000000000..f169f0f11 --- /dev/null +++ b/src/commands/editContainer/ContainerEditContext.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ExecuteActivityContext } from "@microsoft/vscode-azext-utils"; +import { type SetTelemetryProps } from "../../telemetry/SetTelemetryProps"; +import { type ContainerUpdateTelemetryProps as TelemetryProps } from "../../telemetry/commandTelemetryProps"; +import { type IContainerAppContext } from "../IContainerAppContext"; +import { type ImageSourceBaseContext } from "../image/imageSource/ImageSourceContext"; + +export interface ContainerEditBaseContext extends IContainerAppContext, ImageSourceBaseContext, ExecuteActivityContext { + containersIdx: number; +} + +export type ContainerEditContext = ContainerEditBaseContext & SetTelemetryProps; diff --git a/src/commands/editContainer/ContainerEditDraftStep.ts b/src/commands/editContainer/ContainerEditDraftStep.ts new file mode 100644 index 000000000..5e5ce1d23 --- /dev/null +++ b/src/commands/editContainer/ContainerEditDraftStep.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Container, type Revision } from "@azure/arm-appcontainers"; +import { activityFailContext, activityFailIcon, activityProgressContext, activityProgressIcon, activitySuccessContext, activitySuccessIcon, createUniversallyUniqueContextValue, GenericParentTreeItem, GenericTreeItem, nonNullProp, type ExecuteActivityOutput } from "@microsoft/vscode-azext-utils"; +import { type Progress } from "vscode"; +import { type ContainerAppItem, type ContainerAppModel } from "../../tree/ContainerAppItem"; +import { type RevisionsItemModel } from "../../tree/revisionManagement/RevisionItem"; +import { localize } from "../../utils/localize"; +import { getParentResourceFromItem } from "../../utils/revisionDraftUtils"; +import { getContainerNameForImage } from "../image/imageSource/containerRegistry/getContainerNameForImage"; +import { RevisionDraftUpdateBaseStep } from "../revisionDraft/RevisionDraftUpdateBaseStep"; +import { type ContainerEditContext } from "./ContainerEditContext"; + +export class ContainerEditDraftStep extends RevisionDraftUpdateBaseStep { + public priority: number = 590; + + constructor(baseItem: ContainerAppItem | RevisionsItemModel) { + super(baseItem); + } + + public async execute(context: T, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise { + progress.report({ message: localize('editingContainer', 'Editing container (draft)...') }); + this.revisionDraftTemplate.containers ??= []; + + const container: Container = this.revisionDraftTemplate.containers[context.containersIdx] ?? {}; + container.name = getContainerNameForImage(nonNullProp(context, 'image')); + container.image = context.image; + container.env = context.environmentVariables; + + await this.updateRevisionDraftWithTemplate(context); + } + + public shouldExecute(context: T): boolean { + return context.containersIdx !== undefined && !!context.image; + } + + public createSuccessOutput(): ExecuteActivityOutput { + const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem); + return { + item: new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['containerEditDraftStepSuccessItem', activitySuccessContext]), + label: localize('editContainer', 'Edit container profile for container app "{0}" (draft)', parentResource.name), + iconPath: activitySuccessIcon, + }), + message: localize('editContainerSuccess', 'Successfully edited container profile for container app "{0}" (draft).', parentResource.name), + }; + } + + public createProgressOutput(): ExecuteActivityOutput { + const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem); + return { + item: new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['containerEditDraftStepProgressItem', activityProgressContext]), + label: localize('editContainer', 'Edit container profile for container app "{0}" (draft)', parentResource.name), + iconPath: activityProgressIcon, + }), + }; + } + + public createFailOutput(): ExecuteActivityOutput { + const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem); + return { + item: new GenericParentTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['containerEditDraftStepFailItem', activityFailContext]), + label: localize('editContainer', 'Edit container profile for container app "{0}" (draft)', parentResource.name), + iconPath: activityFailIcon, + }), + message: localize('editContainerFail', 'Failed to edit container profile for container app "{0}" (draft).', parentResource.name), + }; + } +} diff --git a/src/commands/image/updateImage/UpdateRegistryAndSecretsStep.ts b/src/commands/editContainer/RegistryAndSecretsUpdateStep.ts similarity index 83% rename from src/commands/image/updateImage/UpdateRegistryAndSecretsStep.ts rename to src/commands/editContainer/RegistryAndSecretsUpdateStep.ts index 56eed3af0..9d141ecf5 100644 --- a/src/commands/image/updateImage/UpdateRegistryAndSecretsStep.ts +++ b/src/commands/editContainer/RegistryAndSecretsUpdateStep.ts @@ -7,16 +7,16 @@ import { type RegistryCredentials, type Secret } from "@azure/arm-appcontainers" import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils"; import * as deepEqual from "deep-eql"; import { type Progress } from "vscode"; -import { ext } from "../../../extensionVariables"; -import { getContainerEnvelopeWithSecrets, type ContainerAppModel } from "../../../tree/ContainerAppItem"; -import { localize } from "../../../utils/localize"; -import { updateContainerApp } from "../../updateContainerApp"; -import { type UpdateImageContext } from "./updateImage"; +import { ext } from "../../extensionVariables"; +import { getContainerEnvelopeWithSecrets, type ContainerAppModel } from "../../tree/ContainerAppItem"; +import { localize } from "../../utils/localize"; +import { updateContainerApp } from "../updateContainerApp"; +import { type ContainerEditContext } from "./ContainerEditContext"; -export class UpdateRegistryAndSecretsStep extends AzureWizardExecuteStep { +export class RegistryAndSecretsUpdateStep extends AzureWizardExecuteStep { public priority: number = 580; - public async execute(context: UpdateImageContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise { + public async execute(context: T, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise { const containerApp: ContainerAppModel = nonNullProp(context, 'containerApp'); const containerAppEnvelope = await getContainerEnvelopeWithSecrets(context, context.subscription, containerApp); @@ -28,20 +28,17 @@ export class UpdateRegistryAndSecretsStep extends AzureWizardExecuteStep { + const item: ContainerItem | ContainersItem = node ?? await pickContainer(context, { autoSelectDraft: true }); + const { containerApp, subscription } = item; + + if (!isTemplateItemEditable(item)) { + throw new TemplateItemNotEditableError(item); + } + + const subscriptionContext: ISubscriptionContext = createSubscriptionContext(subscription); + const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(item); + + let containersIdx: number; + if (ContainersItem.isContainersItem(item)) { + // The 'editContainer' command should only show up on a 'ContainersItem' when it only has one container, else the command would show up on the 'ContainerItem' + containersIdx = 0; + } else { + containersIdx = item.containersIdx; + } + + const wizardContext: ContainerEditContext = { + ...context, + ...subscriptionContext, + ...await createActivityContext(true), + subscription, + managedEnvironment: await getManagedEnvironmentFromContainerApp({ ...context, ...subscriptionContext }, containerApp), + containerApp, + containersIdx, + }; + wizardContext.telemetry.properties.revisionMode = containerApp.revisionsMode; + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title: localize('editContainer', 'Edit container profile for container app "{0}" (draft)', parentResource.name), + promptSteps: [ + new ImageSourceListStep(), + new RevisionDraftDeployPromptStep(), + ], + executeSteps: [ + getVerifyProvidersStep(), + new RegistryAndSecretsUpdateStep(), + new ContainerEditDraftStep(item), + ], + showLoadingPrompt: true, + }); + + await wizard.prompt(); + await wizard.execute(); +} diff --git a/src/commands/image/imageSource/containerRegistry/ContainerRegistryImageConfigureStep.ts b/src/commands/image/imageSource/containerRegistry/ContainerRegistryImageConfigureStep.ts index c197dace6..3b54f14f2 100644 --- a/src/commands/image/imageSource/containerRegistry/ContainerRegistryImageConfigureStep.ts +++ b/src/commands/image/imageSource/containerRegistry/ContainerRegistryImageConfigureStep.ts @@ -3,15 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; import { parseImageName } from "../../../../utils/imageNameUtils"; +import { localize } from "../../../../utils/localize"; +import { AzureWizardActivityOutputExecuteStep } from "../../../AzureWizardActivityOutputExecuteStep"; import { type ContainerRegistryImageSourceContext } from "./ContainerRegistryImageSourceContext"; import { getLoginServer } from "./getLoginServer"; -export class ContainerRegistryImageConfigureStep extends AzureWizardExecuteStep { +export class ContainerRegistryImageConfigureStep extends AzureWizardActivityOutputExecuteStep { public priority: number = 570; + public stepName: string = 'containerRegistryImageConfigureStep'; + protected getSuccessString = (context: T) => localize('successOutput', 'Successfully set container app image to "{0}".', context.image); + protected getFailString = (context: T) => localize('failOutput', 'Failed to set container app image to "{0}".', context.image); + protected getTreeItemLabel = (context: T) => localize('treeItemLabel', 'Set container app image to "{0}"', context.image); - public async execute(context: ContainerRegistryImageSourceContext): Promise { + public async execute(context: T): Promise { context.image = `${getLoginServer(context)}/${context.repositoryName}:${context.tag}`; const { registryName, registryDomain } = parseImageName(context.image); @@ -19,7 +24,7 @@ export class ContainerRegistryImageConfigureStep extends AzureWizardExecuteStep< context.telemetry.properties.registryDomain = registryDomain ?? 'other'; } - public shouldExecute(context: ContainerRegistryImageSourceContext): boolean { + public shouldExecute(context: T): boolean { return !context.image; } } diff --git a/src/commands/image/updateImage/UpdateImageDraftStep.ts b/src/commands/image/updateImage/UpdateImageDraftStep.ts deleted file mode 100644 index e06331eb7..000000000 --- a/src/commands/image/updateImage/UpdateImageDraftStep.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type Revision } from "@azure/arm-appcontainers"; -import { nonNullProp } from "@microsoft/vscode-azext-utils"; -import { type Progress } from "vscode"; -import { ext } from "../../../extensionVariables"; -import { type ContainerAppItem, type ContainerAppModel } from "../../../tree/ContainerAppItem"; -import { type RevisionsItemModel } from "../../../tree/revisionManagement/RevisionItem"; -import { localize } from "../../../utils/localize"; -import { getParentResourceFromItem } from "../../../utils/revisionDraftUtils"; -import { RevisionDraftUpdateBaseStep } from "../../revisionDraft/RevisionDraftUpdateBaseStep"; -import { getContainerNameForImage } from "../imageSource/containerRegistry/getContainerNameForImage"; -import { type UpdateImageContext } from "./updateImage"; - -export class UpdateImageDraftStep extends RevisionDraftUpdateBaseStep { - public priority: number = 590; - - constructor(baseItem: ContainerAppItem | RevisionsItemModel) { - super(baseItem); - } - - public async execute(context: T, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise { - progress.report({ message: localize('updatingImage', 'Updating image (draft)...') }); - - this.revisionDraftTemplate.containers = []; - this.revisionDraftTemplate.containers.push({ - env: context.environmentVariables, - image: context.image, - name: getContainerNameForImage(nonNullProp(context, 'image')), - }); - - await this.updateRevisionDraftWithTemplate(context); - - const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(this.baseItem); - ext.outputChannel.appendLog(localize('updatedImage', 'Updated container app "{0}" with image "{1}" (draft).', parentResource.name, context.image)); - } - - public shouldExecute(context: T): boolean { - return !!context.containerApp && !!context.image; - } -} diff --git a/src/commands/image/updateImage/updateImage.ts b/src/commands/image/updateImage/updateImage.ts deleted file mode 100644 index b9b108add..000000000 --- a/src/commands/image/updateImage/updateImage.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* 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 Revision } from "@azure/arm-appcontainers"; -import { AzureWizard, createSubscriptionContext, type ExecuteActivityContext, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; -import { ext } from "../../../extensionVariables"; -import { type SetTelemetryProps } from "../../../telemetry/SetTelemetryProps"; -import { type UpdateImageTelemetryProps as TelemetryProps } from "../../../telemetry/commandTelemetryProps"; -import { type ContainerAppItem, type ContainerAppModel } from "../../../tree/ContainerAppItem"; -import { type RevisionDraftItem } from "../../../tree/revisionManagement/RevisionDraftItem"; -import { type RevisionItem } from "../../../tree/revisionManagement/RevisionItem"; -import { createActivityContext } from "../../../utils/activityUtils"; -import { getManagedEnvironmentFromContainerApp } from "../../../utils/getResourceUtils"; -import { getVerifyProvidersStep } from "../../../utils/getVerifyProvidersStep"; -import { localize } from "../../../utils/localize"; -import { pickContainerApp } from "../../../utils/pickItem/pickContainerApp"; -import { pickRevision, pickRevisionDraft } from "../../../utils/pickItem/pickRevision"; -import { getParentResourceFromItem } from "../../../utils/revisionDraftUtils"; -import { RevisionDraftDeployPromptStep } from "../../revisionDraft/RevisionDraftDeployPromptStep"; -import { type ImageSourceBaseContext } from "../imageSource/ImageSourceContext"; -import { ImageSourceListStep } from "../imageSource/ImageSourceListStep"; -import { UpdateImageDraftStep } from "./UpdateImageDraftStep"; -import { UpdateRegistryAndSecretsStep } from "./UpdateRegistryAndSecretsStep"; - -export type UpdateImageContext = ImageSourceBaseContext & ExecuteActivityContext & SetTelemetryProps; - -/** - * An ACA exclusive command that updates the container app or revision's container image via revision draft. - * The draft must be deployed for the changes to take effect and can be used to bundle together template changes. - */ -export async function updateImage(context: IActionContext, node?: ContainerAppItem | RevisionItem): Promise { - let item: ContainerAppItem | RevisionItem | RevisionDraftItem | undefined = node; - if (!item) { - const containerAppItem: ContainerAppItem = await pickContainerApp(context); - - if (containerAppItem.containerApp.revisionsMode === KnownActiveRevisionsMode.Single) { - item = containerAppItem; - } else { - if (ext.revisionDraftFileSystem.doesContainerAppsItemHaveRevisionDraft(containerAppItem)) { - item = await pickRevisionDraft(context, containerAppItem); - } else { - item = await pickRevision(context, containerAppItem); - } - } - } - - const { subscription, containerApp } = item; - const subscriptionContext: ISubscriptionContext = createSubscriptionContext(subscription); - - const wizardContext: UpdateImageContext = { - ...context, - ...subscriptionContext, - ...await createActivityContext(), - subscription, - managedEnvironment: await getManagedEnvironmentFromContainerApp({ ...context, ...subscriptionContext }, containerApp), - containerApp - }; - - wizardContext.telemetry.properties.revisionMode = containerApp.revisionsMode; - - const parentResource: ContainerAppModel | Revision = getParentResourceFromItem(item); - const wizard: AzureWizard = new AzureWizard(wizardContext, { - title: localize('updateImage', 'Update container image for "{0}" (draft)', parentResource.name), - promptSteps: [ - new ImageSourceListStep(), - new RevisionDraftDeployPromptStep(), - ], - executeSteps: [ - getVerifyProvidersStep(), - new UpdateRegistryAndSecretsStep(), - new UpdateImageDraftStep(item), - ], - showLoadingPrompt: true - }); - - await wizard.prompt(); - await wizard.execute(); -} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 00b99b187..f4d17f1e0 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -11,6 +11,7 @@ import { createManagedEnvironment } from './createManagedEnvironment/createManag import { deleteContainerApp } from './deleteContainerApp/deleteContainerApp'; import { deleteManagedEnvironment } from './deleteManagedEnvironment/deleteManagedEnvironment'; import { deployWorkspaceProject } from './deployWorkspaceProject/deployWorkspaceProject'; +import { editContainer } from './editContainer/editContainer'; import { editContainerApp } from './editContainerApp'; import { connectToGitHub } from './gitHub/connectToGitHub/connectToGitHub'; import { disconnectRepo } from './gitHub/disconnectRepo/disconnectRepo'; @@ -18,7 +19,6 @@ import { openGitHubRepo } from './gitHub/openGitHubRepo'; import { deployImageApi } from './image/deployImageApi/deployImageApi'; import { createAcr } from './image/imageSource/containerRegistry/acr/createAcr/createAcr'; import { openAcrBuildLogs } from './image/openAcrBuildLogs'; -import { updateImage } from './image/updateImage/updateImage'; import { disableIngress } from './ingress/disableIngress/disableIngress'; import { editTargetPort } from './ingress/editTargetPort/editTargetPort'; import { enableIngress } from './ingress/enableIngress/enableIngress'; @@ -58,12 +58,14 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping('containerApps.deleteContainerApp', deleteContainerApp); registerCommandWithTreeNodeUnwrapping('containerApps.editContainerApp', editContainerApp); registerCommandWithTreeNodeUnwrapping('containerApps.openConsoleInPortal', openConsoleInPortal); - registerCommandWithTreeNodeUnwrapping('containerApps.updateImage', updateImage); registerCommandWithTreeNodeUnwrapping('containerApps.toggleEnvironmentVariableVisibility', async (context: IActionContext, item: EnvironmentVariableItem) => { await item.toggleValueVisibility(context); }); + // containers + registerCommandWithTreeNodeUnwrapping('containerApps.editContainer', editContainer); + // deploy registerCommandWithTreeNodeUnwrapping('containerApps.deployImageApi', deployImageApi); registerCommandWithTreeNodeUnwrapping('containerApps.deployRevisionDraft', deployRevisionDraft); diff --git a/src/commands/scaling/scaleRange/editScaleRange.ts b/src/commands/scaling/scaleRange/editScaleRange.ts index a0a09a381..75c9c4394 100644 --- a/src/commands/scaling/scaleRange/editScaleRange.ts +++ b/src/commands/scaling/scaleRange/editScaleRange.ts @@ -11,7 +11,7 @@ import { type ScaleItem } from "../../../tree/scaling/ScaleItem"; import { createActivityContext } from "../../../utils/activityUtils"; import { localize } from "../../../utils/localize"; import { pickScale } from "../../../utils/pickItem/pickScale"; -import { getParentResource, isTemplateItemEditable, throwTemplateItemNotEditable } from "../../../utils/revisionDraftUtils"; +import { getParentResource, isTemplateItemEditable, TemplateItemNotEditableError } from "../../../utils/revisionDraftUtils"; import { RevisionDraftDeployPromptStep } from "../../revisionDraft/RevisionDraftDeployPromptStep"; import { type ScaleRangeContext } from "./ScaleRangeContext"; import { ScaleRangePromptStep } from "./ScaleRangePromptStep"; @@ -22,7 +22,7 @@ export async function editScaleRange(context: IActionContext, node?: ScaleItem): const { containerApp, revision, subscription } = item; if (!isTemplateItemEditable(item)) { - throwTemplateItemNotEditable(item); + throw new TemplateItemNotEditableError(item); } const parentResource: ContainerAppModel | Revision = getParentResource(containerApp, revision); diff --git a/src/telemetry/commandTelemetryProps.ts b/src/telemetry/commandTelemetryProps.ts index adca1a261..531cfa236 100644 --- a/src/telemetry/commandTelemetryProps.ts +++ b/src/telemetry/commandTelemetryProps.ts @@ -19,7 +19,7 @@ export interface DeployRevisionDraftTelemetryProps extends AzdTelemetryProps, Ov directUpdatesCount?: string; // Direct updates via 'editContainerApp' & 'editDraft' } -export interface UpdateImageTelemetryProps extends AzdTelemetryProps, ImageSourceTelemetryProps { +export interface ContainerUpdateTelemetryProps extends AzdTelemetryProps, ImageSourceTelemetryProps { revisionMode?: KnownActiveRevisionsMode; skippedRegistryCredentialUpdate?: 'true' | 'false'; } diff --git a/src/tree/containers/ContainersItem.ts b/src/tree/containers/ContainersItem.ts index 43a3a3452..dc76977a5 100644 --- a/src/tree/containers/ContainersItem.ts +++ b/src/tree/containers/ContainersItem.ts @@ -62,7 +62,7 @@ export class ContainersItem extends RevisionDraftDescendantBase { } private get contextValue(): string { - return ContainersItem.contextValue; + return this.parentResource.template?.containers?.length === 1 ? ContainerItem.contextValue : ContainersItem.contextValue; } private get parentResource(): ContainerAppModel | Revision { diff --git a/src/utils/pickItem/pickContainer.ts b/src/utils/pickItem/pickContainer.ts new file mode 100644 index 000000000..5839935e0 --- /dev/null +++ b/src/utils/pickItem/pickContainer.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { KnownActiveRevisionsMode, type Container } from "@azure/arm-appcontainers"; +import { AzureWizardPromptStep, ContextValueQuickPickStep, runQuickPickWizard, type AzureResourceQuickPickWizardContext, type IActionContext, type IWizardOptions, type QuickPickWizardContext } from "@microsoft/vscode-azext-utils"; +import { ext } from "../../extensionVariables"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { ContainerItem } from "../../tree/containers/ContainerItem"; +import { ContainersItem } from "../../tree/containers/ContainersItem"; +import { RevisionDraftItem } from "../../tree/revisionManagement/RevisionDraftItem"; +import { RevisionItem } from "../../tree/revisionManagement/RevisionItem"; +import { localize } from "../localize"; +import { pickContainerApp } from "./pickContainerApp"; +import { type RevisionDraftPickItemOptions } from "./PickItemOptions"; +import { getPickRevisionDraftStep, getPickRevisionStep, getPickRevisionsStep } from "./pickRevision"; + +function getPickContainerStep(): AzureWizardPromptStep { + return new ContextValueQuickPickStep(ext.rgApiV2.resources.azureResourceTreeDataProvider, { + contextValueFilter: { include: ContainerItem.contextValueRegExp }, + skipIfOne: true, + }, { + placeHolder: localize('selectContainer', 'Select a container'), + }); +} + +function getPickContainersStep(): AzureWizardPromptStep { + return new ContextValueQuickPickStep(ext.rgApiV2.resources.azureResourceTreeDataProvider, { + contextValueFilter: { include: ContainersItem.contextValueRegExp }, + skipIfOne: true, + }); +} + +export async function pickContainer(context: IActionContext, options?: RevisionDraftPickItemOptions): Promise { + const containerAppItem: ContainerAppItem = await pickContainerApp(context); + return await runQuickPickWizard(context, { + promptSteps: getPickContainerSteps(containerAppItem, { autoSelectDraft: options?.autoSelectDraft }), + title: options?.title, + }, containerAppItem); +} + +/** + * Assumes starting from the ContainerAppItem + */ +export function getPickContainerSteps(containerAppItem: ContainerAppItem, options?: RevisionDraftPickItemOptions): AzureWizardPromptStep[] { + const promptSteps: AzureWizardPromptStep[] = []; + if (containerAppItem.containerApp.revisionsMode === KnownActiveRevisionsMode.Multiple) { + promptSteps.push(getPickRevisionsStep()); + + if (options?.autoSelectDraft && ext.revisionDraftFileSystem.doesContainerAppsItemHaveRevisionDraft(containerAppItem)) { + promptSteps.push(getPickRevisionDraftStep()); + } else { + promptSteps.push(getPickRevisionStep()); + } + } + + promptSteps.push(new ContainerItemPickSteps()); + return promptSteps; +} + +export class ContainerItemPickSteps extends AzureWizardPromptStep { + public async prompt(): Promise { + // Nothing to prompt, just need to use the subwizard + } + + public shouldPrompt(): boolean { + return false; + } + + public async getSubWizard(context: T): Promise | undefined> { + const lastNode: unknown = context.pickedNodes.at(-1); + + let containers: Container[] = []; + if (ContainerAppItem.isContainerAppItem(lastNode)) { + containers = lastNode.containerApp.template?.containers ?? []; + } else if (RevisionItem.isRevisionItem(lastNode)) { + containers = lastNode.revision.template?.containers ?? []; + } else if (RevisionDraftItem.isRevisionDraftItem(lastNode)) { + containers = lastNode.revision.template?.containers ?? []; + } + + const promptSteps: AzureWizardPromptStep[] = []; + if (containers.length > 1) { + promptSteps.push(getPickContainersStep()); + } + promptSteps.push(getPickContainerStep()); + + return { promptSteps }; + } +} diff --git a/src/utils/revisionDraftUtils.ts b/src/utils/revisionDraftUtils.ts index 8e34b71e1..9de6f3bed 100644 --- a/src/utils/revisionDraftUtils.ts +++ b/src/utils/revisionDraftUtils.ts @@ -47,6 +47,8 @@ export function isTemplateItemEditable(item: RevisionsItemModel): boolean { /** * If a template item is not editable, throw this error to cancel and alert the user */ -export function throwTemplateItemNotEditable(item: RevisionsItemModel) { - throw new Error(localize('itemNotEditable', 'Action cannot be performed on revision "{0}" because a draft is currently active.', item.revision.name)); +export class TemplateItemNotEditableError extends Error { + constructor(item: RevisionsItemModel) { + super(localize('itemNotEditable', 'Action cannot be performed on revision "{0}" because a draft is currently active.', item.revision.name)); + } }