Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions apps/vs-code-designer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"@azure/arm-appinsights": "^5.0.0-beta.7",
"@azure/arm-appservice": "^15.0.0",
"@azure/arm-resourcegraph": "^5.0.0-beta.3",
"@azure/arm-resources": "^7.0.0",
"@azure/arm-storage": "^18.1.0",
"@azure/arm-subscriptions": "^6.0.0",
"@azure/core-client": "^1.7.3",
"@azure/core-rest-pipeline": "^1.11.0",
"@azure/identity": "^4.5.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,54 @@ import type { SlotTreeItem } from '../../tree/slotsTree/SlotTreeItem';
import { SubscriptionTreeItem } from '../../tree/subscriptionTree/subscriptionTreeItem';
import { isString } from '@microsoft/logic-apps-shared';
import { callWithTelemetryAndErrorHandling, type AzExtParentTreeItem, type IActionContext } from '@microsoft/vscode-azext-utils';
import type { ICreateLogicAppContext } from '@microsoft/vscode-extension-logic-apps';
import type { ICreateLogicAppContext, ILogicAppWizardContext } from '@microsoft/vscode-extension-logic-apps';
import { type MessageItem, window } from 'vscode';

/**
* Creates a Logic App without showing wizard prompts - all required information must be provided in the context.
* @param context - Context with pre-filled values (newSiteName, location, resourceGroup or newResourceGroupName)
* @param subscription - Subscription ID or tree item
* @param skipNotification - If true, skips the completion notification
* @returns The created Logic App tree item
*/
export async function createLogicAppWithoutWizard(
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't SubscriptionTreeItem.createChild already skip prompts if the values are provided in context? And if not, would it make sense to modify that method to skip prompts when not needed instead of adding this one?

context: IActionContext & Partial<ICreateLogicAppContext> & { newSiteName: string; location: string },
subscription: AzExtParentTreeItem | string,
skipNotification?: boolean
): Promise<SlotTreeItem> {
let node: AzExtParentTreeItem | undefined;

if (isString(subscription)) {
node = await ext.rgApi.appResourceTree.findTreeItem(`/subscriptions/${subscription}`, context);
if (!node) {
throw new Error(localize('noMatchingSubscription', 'Failed to find a subscription matching id "{0}".', subscription));
}
} else {
node = subscription;
}

try {
// Call the subscription tree item's createChild with a special flag to skip prompts
const logicAppNode: SlotTreeItem = await SubscriptionTreeItem.createChildWithoutPrompts(
context as unknown as ICreateLogicAppContext & ILogicAppWizardContext,
node as SubscriptionTreeItem
);

if (!skipNotification) {
await notifyCreateLogicAppComplete(logicAppNode);
}

// The node returned from creation may not have fully initialized fullId
// Look it up from the tree to ensure all properties are correctly set
const fullResourceId = `/subscriptions/${subscription}/resourceGroups/${context.newResourceGroupName ?? context.resourceGroup.name}/providers/Microsoft.Web/sites/${context.newSiteName}`;
const refetchedLogicAppNode = (await ext.rgApi.appResourceTree.findTreeItem(fullResourceId, context as IActionContext)) as SlotTreeItem;

return refetchedLogicAppNode;
} catch (error) {
throw new Error(`Error in creating logic app. ${error}`);
}
}

export async function createLogicApp(
context: IActionContext & Partial<ICreateLogicAppContext>,
subscription?: AzExtParentTreeItem | string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export class LogicAppCreateStep extends AzureWizardExecuteStep<ILogicAppWizardCo
siteConfig: await this.getNewSiteConfig(context),
reserved: context.newSiteOS === WebsiteOS.linux,
identity: context.customLocation ? undefined : { type: 'SystemAssigned' },
httpsOnly: true,
vnetRouteAllEnabled: false, // Enable VNet route all for enhanced security
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

Inconsistent handling of vnetRouteAllEnabled property. The comment says "Enable VNet route all for enhanced security" but the value is set to false, which disables the feature. Either the comment is incorrect or the value should be true.

Suggested change
vnetRouteAllEnabled: false, // Enable VNet route all for enhanced security
vnetRouteAllEnabled: true, // Enable VNet route all for enhanced security

Copilot uses AI. Check for mistakes.
};

if (context.customLocation) {
Expand Down
24 changes: 22 additions & 2 deletions apps/vs-code-designer/src/app/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function deploySlot(
await deploy(context, target, functionAppId, new RegExp(LogicAppResourceTree.pickSlotContextValue));
}

async function deploy(
export async function deploy(
actionContext: IActionContext,
target: Uri | string | SlotTreeItem | undefined,
functionAppId: string | Record<string, any> | undefined,
Expand All @@ -100,7 +100,27 @@ async function deploy(

let node: SlotTreeItem;

if (expectedContextValue) {
// If functionAppId is a SlotTreeItem or LogicAppResourceTree, convert/use it directly
if (functionAppId && typeof functionAppId === 'object') {
const objWithConstructor = functionAppId as any;
if (objWithConstructor.constructor?.name === 'LogicAppResourceTree') {
// It's a LogicAppResourceTree, need to wrap it in SlotTreeItem
const resourceTree = functionAppId as any as LogicAppResourceTree;
const parentTreeItem = (resourceTree as any).parent;
if (!parentTreeItem) {
throw new Error('LogicAppResourceTree missing parent tree item');
}
node = new SlotTreeItem(parentTreeItem, resourceTree);
} else if ('resourceTree' in objWithConstructor && 'site' in objWithConstructor) {
// It's already a SlotTreeItem
node = functionAppId as SlotTreeItem;
} else {
// Unknown object type, fall through to normal path
node = await getDeployNode(context, ext.rgApi.appResourceTree, target, functionAppId, async () =>
getDeployLogicAppNode(actionContext)
);
}
} else if (expectedContextValue) {
node = await getDeployNode(context, ext.rgApi.appResourceTree, target, functionAppId, async () =>
ext.rgApi.pickAppResource(
{ ...context, suppressCreatePick: false },
Expand Down
151 changes: 151 additions & 0 deletions apps/vs-code-designer/src/app/commands/deploy/deployWebview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { IActionContext, AzExtParentTreeItem } from '@microsoft/vscode-azext-utils';
import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
import { ExtensionCommand, ProjectName } from '@microsoft/vscode-extension-logic-apps';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { createWorkspaceWebviewCommandHandler } from '../shared/workspaceWebviewCommandHandler';
import type * as vscode from 'vscode';
import { tryGetWebviewPanel } from '../../utils/codeless/common';
import { getAuthorizationToken } from '../../utils/codeless/getAuthorizationToken';
import { deploy } from './deploy';
import { createLogicAppWithoutWizard } from '../createLogicApp/createLogicApp';
import type { SlotTreeItem } from '../../tree/slotsTree/SlotTreeItem';
import { getWebLocations, AppKind } from '@microsoft/vscode-azext-azureappservice';
import type { SubscriptionTreeItem } from '../../tree/subscriptionTree/subscriptionTreeItem';

export async function deployViaWebview(context: IActionContext, target?: vscode.Uri): Promise<void> {
// Get access token for Azure API calls
const accessToken = await getAuthorizationToken();
const cloudHost = (context as any).environment?.name || 'AzureCloud';

await createWorkspaceWebviewCommandHandler({
panelName: localize('deployToAzure', 'Deploy to Azure'),
panelGroupKey: ext.webViewKey.deploy,
projectName: ProjectName.deploy,
createCommand: ExtensionCommand.deploy,
createHandler: async (actionContext: IActionContext, data: any) => {
if (data.createNew) {
// Get subscription tree item to access environment info
const subscriptionNode = (await ext.rgApi.appResourceTree.findTreeItem(
`/subscriptions/${data.subscriptionId}`,
actionContext
)) as AzExtParentTreeItem;

if (!subscriptionNode) {
throw new Error(localize('noMatchingSubscription', 'Failed to find subscription "{0}".', data.subscriptionId));
}

// Get subscription context from the tree item
const subscriptionTreeItem = subscriptionNode as SubscriptionTreeItem;
const subscriptionContext = subscriptionTreeItem.subscription;

// User wants to create a new Logic App without wizard prompts
const createContext: any = {
...actionContext,
...subscriptionContext, // Include subscription context with environment info
newSiteName: data.newLogicAppName,
location: data.location,
newResourceGroupName: data.isCreatingNewResourceGroup ? data.resourceGroup : undefined,
resourceGroup: data.isCreatingNewResourceGroup ? undefined : { name: data.resourceGroup },
newPlanName: data.isCreatingNewAppServicePlan ? data.appServicePlan : undefined,
plan: data.isCreatingNewAppServicePlan ? undefined : { id: data.appServicePlan },
appServicePlanSku: data.appServicePlanSku || 'WS1',
newStorageAccountName: data.isCreatingNewStorageAccount ? data.storageAccount : undefined,
storageAccount: data.isCreatingNewStorageAccount ? undefined : { id: data.storageAccount },
createAppInsights: data.createAppInsights,
newAppInsightsName: data.appInsightsName,
};

// Create the Logic App using the wizard-free method
const node: SlotTreeItem = await createLogicAppWithoutWizard(
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't createLogicApp called from deploy if the logic app doesn't exist? Can this be refactored so that the logic app is either always created prior to deploy or always created within deploy function?

createContext,
data.subscriptionId,
true // Skip notification since we're deploying next
);

// Mark as new app to skip overwrite confirmation and track webview deployment
(actionContext as any).isNewApp = true;
actionContext.telemetry.properties.deploymentSource = 'webview';
actionContext.telemetry.properties.isNewLogicApp = 'true';

await deploy(actionContext, target, node);

// Now deploy to the newly created Logic App
// Pass target (workspace Uri) and the node as functionAppId
// await deploy(actionContext, target, node);
} else {
// Deploy to existing Logic App - find the node first
const logicAppNode = (await ext.rgApi.appResourceTree.findTreeItem(data.logicAppId, actionContext)) as SlotTreeItem;
if (!logicAppNode) {
throw new Error(localize('noMatchingLogicApp', 'Failed to find Logic App "{0}".', data.logicAppId));
}

// Track webview deployment to existing app
actionContext.telemetry.properties.deploymentSource = 'webview';
actionContext.telemetry.properties.isNewLogicApp = 'false';

// Pass target (workspace Uri) and logicAppNode as functionAppId
await deploy(actionContext, target, logicAppNode);
}
},
extraHandlers: {
[ExtensionCommand.cancel_deploy]: async () => {
// Close the webview panel
const existingPanel = tryGetWebviewPanel(ext.webViewKey.deploy, localize('deployToAzure', 'Deploy to Azure'));
if (existingPanel) {
existingPanel.dispose();
}
},
[ExtensionCommand.getFilteredLocations]: async (message: any) => {
ext.outputChannel.appendLine(`[DEBUG] getFilteredLocations handler called with data: ${JSON.stringify(message.data)}`);

const panel = tryGetWebviewPanel(ext.webViewKey.deploy, localize('deployToAzure', 'Deploy to Azure'));
if (!panel) {
ext.outputChannel.appendLine('[DEBUG] Panel not found');
return;
}

await callWithTelemetryAndErrorHandling('getFilteredLocations', async (actionContext: IActionContext) => {
try {
const subscriptionId = message.data?.subscriptionId;
ext.outputChannel.appendLine(`[DEBUG] Subscription ID: ${subscriptionId}`);

// Get filtered locations matching the Logic Apps requirements
const wizardContext: any = {
...actionContext,
subscriptionId: subscriptionId,
newSiteKind: AppKind.workflowapp,
newPlanSku: { tier: 'ElasticPremium' },
};

ext.outputChannel.appendLine('[DEBUG] Calling getWebLocations...');
const locations = await getWebLocations(wizardContext);
ext.outputChannel.appendLine(`[DEBUG] Got ${locations.length} locations`);

const filteredLocations = locations.map((loc: any) => ({
name: loc.name,
displayName: loc.displayName,
}));

ext.outputChannel.appendLine(`[DEBUG] Sending response with ${filteredLocations.length} locations`);
panel.webview.postMessage({
command: 'getFilteredLocationsResult',
locations: filteredLocations,
});
} catch (error) {
ext.outputChannel.appendLine(`[DEBUG] Error: ${error}`);
panel.webview.postMessage({
command: 'getFilteredLocationsResult',
error: (error as Error).message,
});
}
});
},
},
extraInitializeData: {
deploymentFolderPath: ext.deploymentFolderPath || target?.fsPath,
accessToken,
cloudHost,
},
});
}
5 changes: 3 additions & 2 deletions apps/vs-code-designer/src/app/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import { createWorkflow } from './createWorkflow/createWorkflow';
import { createNewDataMapCmd, loadDataMapFileCmd } from './dataMapper/dataMapper';
import { deleteLogicApp } from './deleteLogicApp/deleteLogicApp';
import { deleteNode } from './deleteNode';
import { deployProductionSlot, deploySlot } from './deploy/deploy';
import { deploySlot } from './deploy/deploy';
import { deployViaWebview } from './deploy/deployWebview';
import { connectToGitHub } from './deployments/connectToGitHub';
import { disconnectRepo } from './deployments/disconnectRepo';
import { redeployDeployment } from './deployments/redeployDeployment';
Expand Down Expand Up @@ -91,7 +92,7 @@ export function registerCommands(): void {
registerCommand(extensionCommand.createWorkflow, createWorkflow);
registerCommandWithTreeNodeUnwrapping(extensionCommand.createLogicApp, createLogicApp);
registerCommandWithTreeNodeUnwrapping(extensionCommand.createLogicAppAdvanced, createLogicAppAdvanced);
registerSiteCommand(extensionCommand.deploy, unwrapTreeNodeCommandCallback(deployProductionSlot));
registerCommand(extensionCommand.deploy, deployViaWebview);
registerSiteCommand(extensionCommand.deploySlot, unwrapTreeNodeCommandCallback(deploySlot));
registerCommand(extensionCommand.generateDeploymentScripts, generateDeploymentScripts);
registerSiteCommand(extensionCommand.redeploy, unwrapTreeNodeCommandCallback(redeployDeployment));
Expand Down
Loading
Loading