diff --git a/package.json b/package.json index c57e5b46e..240a2ab04 100644 --- a/package.json +++ b/package.json @@ -206,6 +206,11 @@ "title": "%containerApps.openConsoleInPortal%", "category": "Azure Container Apps" }, + { + "command": "containerApps.openExpressPortal", + "title": "%containerApps.openExpressPortal%", + "category": "Azure Container Apps" + }, { "command": "containerApps.editContainer", "title": "%containerApps.editContainer%", @@ -451,6 +456,11 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem/i", "group": "6@2" }, + { + "command": "containerApps.openExpressPortal", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem(.*)express/i", + "group": "9@2" + }, { "command": "containerApps.openConsoleInPortal", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /containerAppItem/i", diff --git a/package.nls.json b/package.nls.json index 9b7d268c3..71f7ae91a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -51,6 +51,7 @@ "containerApps.deleteConfirmation.ClickButton": "Prompts with a warning dialog where you click a button to delete.", "containerApps.openInPortal": "Open in Portal", "containerApps.openConsoleInPortal": "Open Console in Portal", + "containerApps.openExpressPortal": "Open in Express Portal", "containerApps.editScaleRange": "Edit Scaling Range...", "containerApps.addScaleRule": "Add Scale Rule...", "containerApps.deleteScaleRule": "Delete Scale Rule...", diff --git a/src/commands/openExpressPortal.ts b/src/commands/openExpressPortal.ts new file mode 100644 index 000000000..30c3e5cb7 --- /dev/null +++ b/src/commands/openExpressPortal.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 IActionContext } from "@microsoft/vscode-azext-utils"; +import { env, Uri } from "vscode"; +import { expressPortalBaseUrl } from "../constants"; +import { type ContainerAppItem } from "../tree/ContainerAppItem"; +import { pickContainerApp } from "../utils/pickItem/pickContainerApp"; + +export async function openExpressPortal(context: IActionContext, node?: ContainerAppItem): Promise { + node ??= await pickContainerApp(context); + const url = `${expressPortalBaseUrl}/${node.subscription.subscriptionId}/${node.containerApp.resourceGroup}/${node.containerApp.name}/overview`; + await env.openExternal(Uri.parse(url)); +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 70d7a2363..c92f2b68f 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -35,6 +35,7 @@ import { toggleIngressVisibility } from './ingress/toggleIngressVisibility/toggl import { startStreamingLogs } from './logStream/startStreamingLogs'; import { stopStreamingLogs } from './logStream/stopStreamingLogs'; import { openConsoleInPortal } from './openConsoleInPortal'; +import { openExpressPortal } from './openExpressPortal'; import { revealInEnvironment } from './revealInEnvironment'; import { activateRevision } from './revision/activateRevision'; import { chooseRevisionMode } from './revision/chooseRevisionMode/chooseRevisionMode'; @@ -67,6 +68,7 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping('containerApps.deleteContainerApp', deleteContainerApp); registerCommandWithTreeNodeUnwrapping('containerApps.editContainerApp', editContainerApp); registerCommandWithTreeNodeUnwrapping('containerApps.openConsoleInPortal', openConsoleInPortal); + registerCommandWithTreeNodeUnwrapping('containerApps.openExpressPortal', openExpressPortal); registerCommandWithTreeNodeUnwrapping('containerApps.revealInEnvironment', revealInEnvironment); registerCommandWithTreeNodeUnwrapping('containerApps.toggleEnvironmentVariableVisibility', async (context: IActionContext, item: EnvironmentVariableItem) => { diff --git a/src/constants.ts b/src/constants.ts index bc630624a..42e9737aa 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -91,3 +91,6 @@ export const unsavedChangesTrueContextValue: string = 'unsavedChanges:true'; export const unsavedChangesFalseContextValue: string = 'unsavedChanges:false'; export const activityInfoContext: string = 'activity:info'; + +export const expressContextValue: string = 'express'; +export const expressPortalBaseUrl: string = 'https://containerapps.azure.com/apps'; diff --git a/src/tree/ContainerAppItem.ts b/src/tree/ContainerAppItem.ts index 59b31ba60..4f0883bd0 100644 --- a/src/tree/ContainerAppItem.ts +++ b/src/tree/ContainerAppItem.ts @@ -11,7 +11,7 @@ import deepEqual from "deep-eql"; import { TreeItemCollapsibleState, type TreeItem, type Uri } from "vscode"; import { DeleteAllContainerAppsStep } from "../commands/deleteContainerApp/DeleteAllContainerAppsStep"; import { type IDeleteContainerAppWizardContext } from "../commands/deleteContainerApp/IDeleteContainerAppWizardContext"; -import { revisionModeMultipleContextValue, revisionModeSingleContextValue, unsavedChangesFalseContextValue, unsavedChangesTrueContextValue } from "../constants"; +import { expressContextValue, revisionModeMultipleContextValue, revisionModeSingleContextValue, unsavedChangesFalseContextValue, unsavedChangesTrueContextValue } from "../constants"; import { ext } from "../extensionVariables"; import { createActivityContext } from "../utils/activityUtils"; import { createContainerAppsAPIClient, createContainerAppsClient } from "../utils/azureClients"; @@ -47,7 +47,7 @@ export class ContainerAppItem implements ContainerAppsItem, RevisionsDraftModel return this._containerApp; } - constructor(public readonly subscription: AzureSubscription, private _containerApp: ContainerAppModel) { + constructor(public readonly subscription: AzureSubscription, private _containerApp: ContainerAppModel, public readonly isExpress: boolean = false) { this.id = this.containerApp.id; this.resourceGroup = this.containerApp.resourceGroup; this.name = this.containerApp.name; @@ -67,6 +67,10 @@ export class ContainerAppItem implements ContainerAppsItem, RevisionsDraftModel values.push(this.containerApp.revisionsMode === KnownActiveRevisionsMode.Single ? revisionModeSingleContextValue : revisionModeMultipleContextValue); values.push(this.hasUnsavedChanges() ? unsavedChangesTrueContextValue : unsavedChangesFalseContextValue); + if (this.isExpress) { + values.push(expressContextValue); + } + return createContextValue(values); } @@ -79,6 +83,10 @@ export class ContainerAppItem implements ContainerAppsItem, RevisionsDraftModel return this.containerApp.provisioningState; } + if (this.isExpress) { + return localize('express', '(Express)'); + } + return undefined; } diff --git a/src/tree/ManagedEnvironmentItem.ts b/src/tree/ManagedEnvironmentItem.ts index 19ba5ae6a..eb75c25da 100644 --- a/src/tree/ManagedEnvironmentItem.ts +++ b/src/tree/ManagedEnvironmentItem.ts @@ -8,21 +8,26 @@ import { getResourceGroupFromId, uiUtils } from "@microsoft/vscode-azext-azureut import { callWithTelemetryAndErrorHandling, createContextValue, createSubscriptionContext, nonNullProp, nonNullValueAndProp, type IActionContext } from "@microsoft/vscode-azext-utils"; import { type AzureResource, type AzureSubscription, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; import { TreeItemCollapsibleState, type TreeItem } from "vscode"; -import { createContainerAppsAPIClient } from "../utils/azureClients"; +import { createContainerAppsAPIClient, createContainerAppsPreviewAPIClient } from "../utils/azureClients"; +import { localize } from "../utils/localize"; import { treeUtils } from "../utils/treeUtils"; import { ContainerAppItem } from "./ContainerAppItem"; import { type ContainerAppsItem, type TreeElementBase } from "./ContainerAppsBranchDataProvider"; -type ManagedEnvironmentModel = ManagedEnvironment & ResourceModel; +type ManagedEnvironmentModel = ManagedEnvironment & ResourceModel & { + environmentMode?: string; +}; export class ManagedEnvironmentItem implements TreeElementBase { static readonly contextValue: string = 'managedEnvironmentItem'; static readonly contextValueRegExp: RegExp = new RegExp(ManagedEnvironmentItem.contextValue); viewProperties: ViewPropertiesModel; id: string; + readonly isExpress: boolean; constructor(public readonly subscription: AzureSubscription, public readonly resource: AzureResource, public readonly managedEnvironment: ManagedEnvironmentModel) { + this.isExpress = managedEnvironment.environmentMode === 'Express'; this.id = managedEnvironment.id; this.viewProperties = { data: managedEnvironment, @@ -53,7 +58,7 @@ export class ManagedEnvironmentItem implements TreeElementBase { context.valuesToMask.push(...containerApps.map(ca => ca.name), ...containerApps.map(ca => ca.id)); return containerApps - .map(ca => new ContainerAppItem(this.subscription, ca)) + .map(ca => new ContainerAppItem(this.subscription, ca, this.isExpress)) .sort((a, b) => treeUtils.sortById(a, b)); }); @@ -63,6 +68,7 @@ export class ManagedEnvironmentItem implements TreeElementBase { getTreeItem(): TreeItem { return { label: this.managedEnvironment.name, + description: this.isExpress ? localize('express', '(Express)') : undefined, id: this.id, iconPath: treeUtils.getIconPath('managed-environment'), contextValue: this.contextValue, @@ -84,8 +90,21 @@ export class ManagedEnvironmentItem implements TreeElementBase { static async Get(context: IActionContext, subscription: AzureSubscription, resourceGroup: string, name: string): Promise { const subContext = createSubscriptionContext(subscription); - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, subContext]); - return ManagedEnvironmentItem.CreateManagedEnvironmentModel(await client.managedEnvironments.get(resourceGroup, name)); + const client: ContainerAppsAPIClient = await createContainerAppsPreviewAPIClient([context, subContext]); + + let environmentMode: string | undefined; + const result = await client.managedEnvironments.get(resourceGroup, name, { + onResponse: (response) => { + const rawBody = response.bodyAsText; + if (rawBody) { + environmentMode = (JSON.parse(rawBody) as { properties?: { environmentMode?: string } })?.properties?.environmentMode; + } + }, + }); + + const model = ManagedEnvironmentItem.CreateManagedEnvironmentModel(result); + model.environmentMode = environmentMode; + return model; } private static CreateManagedEnvironmentModel(managedEnvironment: ManagedEnvironment): ManagedEnvironmentModel { diff --git a/src/tree/scaling/ScaleItem.ts b/src/tree/scaling/ScaleItem.ts index e96552d8e..6245b5456 100644 --- a/src/tree/scaling/ScaleItem.ts +++ b/src/tree/scaling/ScaleItem.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { KnownActiveRevisionsMode, type Revision, type Scale } from "@azure/arm-appcontainers"; -import { createGenericElement, nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; +import { createGenericElement } from "@microsoft/vscode-azext-utils"; import { type AzureSubscription, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api"; import deepEqual from 'deep-eql'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from "vscode"; @@ -50,12 +50,12 @@ export class ScaleItem extends RevisionDraftDescendantBase { protected setProperties(): void { this.label = scaling; - this.scale = nonNullValueAndProp(this.parentResource.template, 'scale'); + this.scale = this.parentResource.template?.scale ?? {}; } protected setDraftProperties(): void { this.label = `${scaling}*`; - this.scale = nonNullValueAndProp(ext.revisionDraftFileSystem.parseRevisionDraft(this), 'scale'); + this.scale = ext.revisionDraftFileSystem.parseRevisionDraft(this)?.scale ?? {}; } getTreeItem(): TreeItem { diff --git a/src/utils/azureClients.ts b/src/utils/azureClients.ts index 838af021b..26db6a3eb 100644 --- a/src/utils/azureClients.ts +++ b/src/utils/azureClients.ts @@ -24,6 +24,25 @@ export async function createContainerAppsAPIClient(context: AzExtClientContext): return createAzureClient(context, (await import('@azure/arm-appcontainers')).ContainerAppsAPIClient as unknown as AzExtClientType); } +const previewApiVersion = '2026-03-02-preview'; + +export async function createContainerAppsPreviewAPIClient(context: AzExtClientContext): Promise { + const client = await createContainerAppsAPIClient(context); + client.apiVersion = previewApiVersion; + client.pipeline.addPolicy({ + name: 'PreviewApiVersionPolicy', + sendRequest(request, next) { + const [base, query] = request.url.split('?'); + if (query) { + const newQuery = query.split('&').map(p => p.startsWith('api-version=') ? `api-version=${previewApiVersion}` : p).join('&'); + request.url = `${base}?${newQuery}`; + } + return next(request); + }, + }); + return client; +} + export async function createContainerRegistryManagementClient(context: AzExtClientContext): Promise { return createAzureClient(context, (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient); }