diff --git a/package-lock.json b/package-lock.json index c69483d8d..81b9c28f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@microsoft/vscode-azext-azureappsettings": "^0.2.1", "@microsoft/vscode-azext-azureutils": "^3.0.0", "@microsoft/vscode-azext-serviceconnector": "^0.1.3", - "@microsoft/vscode-azext-utils": "^2.3.1", + "@microsoft/vscode-azext-utils": "^2.5.0", "@microsoft/vscode-azureresources-api": "^2.0.4", "cross-fetch": "^4.0.0", "escape-string-regexp": "^4.0.0", @@ -1227,9 +1227,9 @@ } }, "node_modules/@microsoft/vscode-azext-utils": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.3.1.tgz", - "integrity": "sha512-XPEC6CHPJ4czdnYVuD0fk/HKv41ZzCS7CGwNysyDZvp53P64qhdL3FydlvkwwUwmbL0+1kNmMyVvO8xd3y2uXQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.5.0.tgz", + "integrity": "sha512-K++jZjALHfbBsU3E64w/PK8tVibG6g4i0yZcP2wUxkZh053Wf1ZDlMlRSHvuH8oQeOXIcFVRvLwPDqhx8o0cuA==", "dependencies": { "@microsoft/vscode-azureresources-api": "^2.0.4", "@vscode/extension-telemetry": "^0.9.0", @@ -14270,9 +14270,9 @@ } }, "@microsoft/vscode-azext-utils": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.3.1.tgz", - "integrity": "sha512-XPEC6CHPJ4czdnYVuD0fk/HKv41ZzCS7CGwNysyDZvp53P64qhdL3FydlvkwwUwmbL0+1kNmMyVvO8xd3y2uXQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.5.0.tgz", + "integrity": "sha512-K++jZjALHfbBsU3E64w/PK8tVibG6g4i0yZcP2wUxkZh053Wf1ZDlMlRSHvuH8oQeOXIcFVRvLwPDqhx8o0cuA==", "requires": { "@microsoft/vscode-azureresources-api": "^2.0.4", "@vscode/extension-telemetry": "^0.9.0", diff --git a/package.json b/package.json index c8fa842dd..80a39a16c 100644 --- a/package.json +++ b/package.json @@ -175,16 +175,6 @@ "command": "azureFunctions.createNewProject", "title": "%azureFunctions.createNewProject%", "category": "Azure Functions", - "icon": { - "light": "resources/light/CreateNewProject.svg", - "dark": "resources/dark/CreateNewProject.svg" - }, - "enablement": "!virtualWorkspace" - }, - { - "command": "azureFunctions.createNewProjectWithDockerfile", - "title": "%azureFunctions.createNewProjectWithDockerfile%", - "category": "Azure Functions", "enablement": "!virtualWorkspace" }, { @@ -207,6 +197,13 @@ "title": "%azureFunctions.deleteSlot%", "category": "Azure Functions" }, + { + "command": "azureFunctions.deployProject", + "title": "%azureFunctions.deployProject%", + "category": "Azure Functions", + "icon": "$(cloud-upload)", + "enablement": "!virtualWorkspace" + }, { "command": "azureFunctions.deploy", "title": "%azureFunctions.deploy%", @@ -375,24 +372,8 @@ "group": "1_projects@1" }, { - "command": "azureFunctions.createNewProject", - "group": "1_projects@2" - }, - { - "command": "azureFunctions.createNewProjectWithDockerfile", - "group": "1_projects@3" - }, - { - "command": "azureFunctions.deploy", + "command": "azureFunctions.deployProject", "group": "2_deploy@1" - }, - { - "command": "azureFunctions.createFunctionApp", - "group": "3_create@1" - }, - { - "command": "azureFunctions.createFunctionAppAdvanced", - "group": "3_create@2" } ], "view/title": [ @@ -403,6 +384,16 @@ } ], "view/item/context": [ + { + "command": "azureFunctions.createFunction", + "when": "view == azureWorkspace && viewItem =~ /azFunc.*ReadWrite;Functions;/i", + "group": "inline" + }, + { + "command": "azureFunctions.createFunction", + "when": "view == azureWorkspace && viewItem =~ /azFunc.*ReadWrite;Functions;/i", + "group": "1@1" + }, { "command": "azureFunctions.createFunctionApp", "when": "view == azureResourceGroups && viewItem =~ /functionapp/i && viewItem =~ /azureResourceTypeGroup/i", @@ -1104,7 +1095,7 @@ "id": "create", "title": "%azureFunctions.walkthrough.functionsStart.create.title%", "completionEvents": [ - "onCommand:azureFunctions.createNewProject" + "onCommand:azureFunctions.createFunction" ], "description": "%azureFunctions.walkthrough.functionsStart.create.description%", "media": { @@ -1194,7 +1185,7 @@ "@microsoft/vscode-azext-azureappsettings": "^0.2.1", "@microsoft/vscode-azext-azureutils": "^3.0.0", "@microsoft/vscode-azext-serviceconnector": "^0.1.3", - "@microsoft/vscode-azext-utils": "^2.3.1", + "@microsoft/vscode-azext-utils": "^2.5.0", "@microsoft/vscode-azureresources-api": "^2.0.4", "cross-fetch": "^4.0.0", "escape-string-regexp": "^4.0.0", diff --git a/package.nls.json b/package.nls.json index 77f215e9b..6192b1571 100644 --- a/package.nls.json +++ b/package.nls.json @@ -13,17 +13,17 @@ "azureFunctions.configureDeploymentSource": "Configure Deployment Source...", "azureFunctions.connectToGitHub": "Connect to GitHub Repository...", "azureFunctions.copyFunctionUrl": "Copy Function Url", - "azureFunctions.createFunction": "Create Function...", + "azureFunctions.createFunction": "Create function locally...", "azureFunctions.createFunctionApp": "Create Function App in Azure...", "azureFunctions.createFunctionAppAdvanced": "Create Function App in Azure... (Advanced)", "azureFunctions.createFunctionAppDetail": "For serverless, event driven apps and automation.", "azureFunctions.createNewProject": "Create New Project...", - "azureFunctions.createNewProjectWithDockerfile": "Create New Containerized Project...", "azureFunctions.createPythonVenv": "Create a virtual environment when creating a new Python project.", "azureFunctions.createSlot": "Create Slot...", "azureFunctions.deleteFunction": "Delete Function...", "azureFunctions.deleteFunctionApp": "Delete Function App...", "azureFunctions.deleteSlot": "Delete Slot...", + "azureFunctions.deployProject": "Deploy to Azure...", "azureFunctions.deploy": "Deploy to Function App...", "azureFunctions.deploySlot": "Deploy to Slot...", "azureFunctions.deploySubpath": "The default subpath of a workspace folder to use when deploying. If set, you will not be prompted for the folder path when deploying.", diff --git a/src/LocalResourceProvider.ts b/src/LocalResourceProvider.ts index a1f6ddcd7..aad877460 100644 --- a/src/LocalResourceProvider.ts +++ b/src/LocalResourceProvider.ts @@ -2,12 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzExtParentTreeItem, type AzExtTreeItem } from "@microsoft/vscode-azext-utils"; +import { GenericTreeItem, type AzExtParentTreeItem, type AzExtTreeItem } from "@microsoft/vscode-azext-utils"; import { type WorkspaceResourceProvider } from "@microsoft/vscode-azext-utils/hostapi"; import { Disposable } from "vscode"; +import { localize } from "./localize"; import { InitLocalProjectTreeItem } from "./tree/localProject/InitLocalProjectTreeItem"; import { InvalidLocalProjectTreeItem } from "./tree/localProject/InvalidLocalProjectTreeItem"; import { LocalProjectTreeItem } from "./tree/localProject/LocalProjectTreeItem"; +import { treeUtils } from "./utils/treeUtils"; import { listLocalProjects, type LocalProjectInternal } from "./workspace/listLocalProjects"; export class FunctionsLocalResourceProvider implements WorkspaceResourceProvider { @@ -21,6 +23,7 @@ export class FunctionsLocalResourceProvider implements WorkspaceResourceProvider this._projectDisposables = []; const localProjects = await listLocalProjects(); + let hasLocalProject = false; for (const project of localProjects.initializedProjects) { const treeItem: LocalProjectTreeItem = new LocalProjectTreeItem(parent, project as LocalProjectInternal); @@ -29,13 +32,26 @@ export class FunctionsLocalResourceProvider implements WorkspaceResourceProvider } for (const unintializedProject of localProjects.unintializedProjects) { + hasLocalProject = true; children.push(new InitLocalProjectTreeItem(parent, unintializedProject.projectPath, unintializedProject.workspaceFolder)); } for (const invalidProject of localProjects.invalidProjects) { + hasLocalProject = true; children.push(new InvalidLocalProjectTreeItem(parent, invalidProject.projectPath, invalidProject.error, invalidProject.workspaceFolder)); } + if (!hasLocalProject && children.length === 0) { + const ti: GenericTreeItem = new GenericTreeItem(parent, { + label: localize('createFunctionLocally', 'Create New Project...'), + commandId: 'azureFunctions.createNewProject', + contextValue: 'createNewProject', + iconPath: treeUtils.getThemedIconPath('CreateNewProject') + }); + ti.commandArgs = []; + children.push(ti); + } + return children; } private _projectDisposables: Disposable[] = []; diff --git a/src/agent/agentIntegration.ts b/src/agent/agentIntegration.ts index 05bdd24d0..1ffd1637d 100644 --- a/src/agent/agentIntegration.ts +++ b/src/agent/agentIntegration.ts @@ -23,8 +23,8 @@ export async function getCommands(): Promise<(WizardCommandConfig | SimpleComman { type: "simple", name: createFunctionProjectCommandName, - commandId: "azureFunctions.createNewProject", - displayName: "Create Function Project", + commandId: "azureFunctions.createFunction", + displayName: "Create Function Locally", intentDescription: "This is best when users ask to create a new function project in VS Code. They may also refer to creating a function project by asking to create a project based upon a function project template.", requiresAzureLogin: true, }, diff --git a/src/commands/SubscriptionListStep.ts b/src/commands/SubscriptionListStep.ts new file mode 100644 index 000000000..ba2379911 --- /dev/null +++ b/src/commands/SubscriptionListStep.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; +import { l10n } from "vscode"; +import { ext } from "../extensionVariables"; +import { type IFuncDeployContext } from "./deploy/deploy"; + +export class SubscriptionListStep extends AzureWizardPromptStep { + private _picks: IAzureQuickPickItem[] = []; + private _oneSubscription: boolean = false; + public async prompt(context: IFuncDeployContext): Promise { + context.subscription = (await context.ui.showQuickPick(this._picks, { placeHolder: l10n.t("Select a subscription") })).data; + } + + public shouldPrompt(_: IFuncDeployContext): boolean { + return !this._oneSubscription; + } + + public async configureBeforePrompt(context: IFuncDeployContext): Promise { + this._picks = await this.getPicks(context); + // auto select if only one subscription + if (this._picks.length === 1) { + this._oneSubscription = true; + context.subscription = this._picks[0].data; + } + } + + private async getPicks(_: IFuncDeployContext): Promise[]> { + return (await ext.rgApi.getSubscriptions(true)).map(s => { + return { label: s.name, description: s.subscriptionId, data: s }; + }); + } +} diff --git a/src/commands/createFunctionApp/UniqueNamePromptStep.ts b/src/commands/createFunctionApp/UniqueNamePromptStep.ts new file mode 100644 index 000000000..88384957c --- /dev/null +++ b/src/commands/createFunctionApp/UniqueNamePromptStep.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; +import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { localize } from "../../localize"; +import { setConsumptionPlanProperties } from "./FunctionAppHostingPlanStep"; +import { type IFunctionAppWizardContext } from "./IFunctionAppWizardContext"; + +export class ConfigureCommonNamesStep extends AzureWizardPromptStep { + public async prompt(_context: IAppServiceWizardContext): Promise { + // do nothing, will be handled in configuration + } + + public shouldPrompt(_context: IAppServiceWizardContext): boolean { + // never prompt + return false; + } + + public async configureBeforePrompt(context: IFunctionAppWizardContext): Promise { + if (!context.advancedCreation) { + const newName: string | undefined = await context.relatedNameTask; + if (!newName) { + throw new Error(localize('noUniqueName', 'Failed to generate unique name for resources. Use advanced creation to manually enter resource names.')); + } + context.newResourceGroupName = context.newResourceGroupName || newName; + setConsumptionPlanProperties(context); + context.newStorageAccountName = newName; + context.newAppInsightsName = newName; + } + } +} diff --git a/src/commands/createFunctionApp/createCreateFunctionAppComponents.ts b/src/commands/createFunctionApp/createCreateFunctionAppComponents.ts new file mode 100644 index 000000000..19c2d5c9a --- /dev/null +++ b/src/commands/createFunctionApp/createCreateFunctionAppComponents.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppInsightsCreateStep, AppInsightsListStep, AppKind, AppServicePlanCreateStep, AppServicePlanListStep, CustomLocationListStep, LogAnalyticsCreateStep, SiteNameStep, WebsiteOS, type IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; +import { LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, StorageAccountCreateStep, StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication, type INewStorageAccountDefaults } from "@microsoft/vscode-azext-azureutils"; +import { type AzureWizardExecuteStep, type AzureWizardPromptStep, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; +import { FuncVersion, latestGAVersion, tryParseFuncVersion } from "../../FuncVersion"; +import { funcVersionSetting } from "../../constants"; +import { tryGetLocalFuncVersion } from "../../funcCoreTools/tryGetLocalFuncVersion"; +import { type ICreateFunctionAppContext } from "../../tree/SubscriptionTreeItem"; +import { createActivityContext } from "../../utils/activityUtils"; +import { getRootFunctionsWorkerRuntime, getWorkspaceSetting, getWorkspaceSettingFromAnyFolder } from "../../vsCodeConfig/settings"; +import { FunctionAppCreateStep } from "./FunctionAppCreateStep"; +import { FunctionAppHostingPlanStep } from "./FunctionAppHostingPlanStep"; +import { type IFunctionAppWizardContext } from "./IFunctionAppWizardContext"; +import { ConfigureCommonNamesStep } from "./UniqueNamePromptStep"; +import { ContainerizedFunctionAppCreateStep } from "./containerImage/ContainerizedFunctionAppCreateStep"; +import { DeployWorkspaceProjectStep } from "./containerImage/DeployWorkspaceProjectStep"; +import { detectDockerfile } from "./containerImage/detectDockerfile"; +import { FunctionAppStackStep } from "./stacks/FunctionAppStackStep"; + +export async function createCreateFunctionAppComponents(context: ICreateFunctionAppContext, + subscription: ISubscriptionContext, + language?: string | undefined): Promise<{ + wizardContext: IFunctionAppWizardContext; + promptSteps: AzureWizardPromptStep[]; + executeSteps: AzureWizardExecuteStep[]; + }> { + + const version: FuncVersion = await getDefaultFuncVersion(context); + context.telemetry.properties.projectRuntime = version; + + const wizardContext: IFunctionAppWizardContext = Object.assign(context, subscription, { + newSiteKind: AppKind.functionapp, + resourceGroupDeferLocationStep: true, + version, + language, + ...(await createActivityContext()) + }); + + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + const storageAccountCreateOptions: INewStorageAccountDefaults = { + kind: StorageAccountKind.Storage, + performance: StorageAccountPerformance.Standard, + replication: StorageAccountReplication.LRS + }; + + await detectDockerfile(context); + + promptSteps.push(new SiteNameStep(context.dockerfilePath ? "containerizedFunctionApp" : "functionApp")); + + if (context.dockerfilePath) { + const containerizedfunctionAppWizard = await createContainerizedFunctionAppWizard(); + promptSteps.push(...containerizedfunctionAppWizard.promptSteps); + executeSteps.push(...containerizedfunctionAppWizard.executeSteps); + } else { + const functionAppWizard = await createFunctionAppWizard(wizardContext); + promptSteps.push(...functionAppWizard.promptSteps); + executeSteps.push(...functionAppWizard.executeSteps); + } + + if (!wizardContext.advancedCreation) { + LocationListStep.addStep(wizardContext, promptSteps); + wizardContext.useConsumptionPlan = true; + wizardContext.stackFilter = getRootFunctionsWorkerRuntime(wizardContext.language); + promptSteps.push(new ConfigureCommonNamesStep()); + executeSteps.push(new ResourceGroupCreateStep()); + executeSteps.push(new StorageAccountCreateStep(storageAccountCreateOptions)); + executeSteps.push(new AppInsightsCreateStep()); + if (!context.dockerfilePath) { + executeSteps.push(new AppServicePlanCreateStep()); + executeSteps.push(new LogAnalyticsCreateStep()); + } + } else { + promptSteps.push(new ResourceGroupListStep()); + promptSteps.push(new StorageAccountListStep( + storageAccountCreateOptions, + { + // The account type must support blobs, queues, and tables. + // See: https://aka.ms/Cfqnrc + kind: [ + // Blob-only accounts don't support queues and tables + StorageAccountKind.BlobStorage + ], + performance: [ + // Premium performance accounts don't support queues and tables + StorageAccountPerformance.Premium + ], + learnMoreLink: 'https://aka.ms/Cfqnrc' + } + )); + promptSteps.push(new AppInsightsListStep()); + } + + + const storageProvider = 'Microsoft.Storage'; + LocationListStep.addProviderForFiltering(wizardContext, storageProvider, 'storageAccounts'); + executeSteps.push(new FunctionAppCreateStep()); + + return { + wizardContext, + promptSteps, + executeSteps + }; +} + +async function getDefaultFuncVersion(context: ICreateFunctionAppContext): Promise { + const settingValue: string | undefined = context.workspaceFolder ? getWorkspaceSetting(funcVersionSetting, context.workspaceFolder) : getWorkspaceSettingFromAnyFolder(funcVersionSetting); + // Try to get VS Code setting for version (aka if they have a project open) + let version: FuncVersion | undefined = tryParseFuncVersion(settingValue); + context.telemetry.properties.runtimeSource = 'VSCodeSetting'; + + if (version === undefined) { + // Try to get the version that matches their local func cli + version = await tryGetLocalFuncVersion(context, undefined); + context.telemetry.properties.runtimeSource = 'LocalFuncCli'; + } + + if (version === undefined) { + version = latestGAVersion; + context.telemetry.properties.runtimeSource = 'Backup'; + } + + return version; +} + +async function createFunctionAppWizard(wizardContext: IFunctionAppWizardContext): Promise<{ promptSteps: AzureWizardPromptStep[], executeSteps: AzureWizardExecuteStep[] }> { + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + if (wizardContext.advancedCreation) { + promptSteps.push(new FunctionAppHostingPlanStep()); + // location is required to get flex runtimes, so prompt before stack step + CustomLocationListStep.addStep(wizardContext, promptSteps); + } + + promptSteps.push(new FunctionAppStackStep()); + + if (wizardContext.advancedCreation) { + promptSteps.push(new AppServicePlanListStep()) + } + + if (wizardContext.version === FuncVersion.v1) { // v1 doesn't support linux + wizardContext.newSiteOS = WebsiteOS.windows; + } + + executeSteps.push(new FunctionAppCreateStep()); + + return { promptSteps, executeSteps }; +} + +async function createContainerizedFunctionAppWizard(): Promise<{ promptSteps: AzureWizardPromptStep[], executeSteps: AzureWizardExecuteStep[] }> { + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + executeSteps.push(new DeployWorkspaceProjectStep()); + executeSteps.push(new ContainerizedFunctionAppCreateStep()); + + return { promptSteps, executeSteps }; +} diff --git a/src/commands/createNewProject/ProjectTypeListStep.ts b/src/commands/createNewProject/ProjectTypeListStep.ts new file mode 100644 index 000000000..879ad6528 --- /dev/null +++ b/src/commands/createNewProject/ProjectTypeListStep.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, UserCancelledError, type IAzureQuickPickItem, type IWizardOptions } from "@microsoft/vscode-azext-utils"; +import { l10n } from "vscode"; +import { validateFuncCoreToolsInstalled } from "../../funcCoreTools/validateFuncCoreToolsInstalled"; +import { localize } from "../../localize"; +import { type IFunctionWizardContext } from "../createFunction/IFunctionWizardContext"; +import { CreateDockerfileProjectStep } from "./dockerfileSteps/CreateDockerfileProjectStep"; + +export class ProjectTypeListStep extends AzureWizardPromptStep { + public async prompt(context: IFunctionWizardContext): Promise { + context.containerizedProject = (await context.ui.showQuickPick(this.getPicks(), { placeHolder: l10n.t('Select a project type') })).data; + } + + public shouldPrompt(): boolean { + return true; + } + + public async getSubWizard(context: IFunctionWizardContext): Promise | undefined> { + if (context.containerizedProject) { + const message: string = localize('installFuncTools', 'You must have the Azure Functions Core Tools installed to run this command.'); + if (!await validateFuncCoreToolsInstalled(context, message)) { + throw new UserCancelledError('validateFuncCoreToolsInstalled'); + } + context.languageFilter = /Python|C\#|(Java|Type)Script|PowerShell$/i; + return { executeSteps: [new CreateDockerfileProjectStep()] }; + } else { + return undefined; + } + } + + private getPicks(): IAzureQuickPickItem[] { + return [ + { label: l10n.t('Create default function project'), description: l10n.t('Recommended'), data: false }, + { label: l10n.t('Generate project with a Dockerfile'), description: l10n.t('Creates a project that will run in a Linux container'), data: true } + ]; + } +} diff --git a/src/commands/createNewProject/createNewProject.ts b/src/commands/createNewProject/createNewProject.ts index aeffbe44d..687a84912 100644 --- a/src/commands/createNewProject/createNewProject.ts +++ b/src/commands/createNewProject/createNewProject.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizard, UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { AzureWizard, type IActionContext } from '@microsoft/vscode-azext-utils'; import { window } from 'vscode'; import { latestGAVersion, tryParseFuncVersion } from '../../FuncVersion'; import { funcVersionSetting, projectLanguageSetting, projectOpenBehaviorSetting, projectTemplateKeySetting, type ProjectLanguage } from '../../constants'; import { ext } from '../../extensionVariables'; import { addLocalFuncTelemetry } from '../../funcCoreTools/getLocalFuncCoreToolsVersion'; import { tryGetLocalFuncVersion } from '../../funcCoreTools/tryGetLocalFuncVersion'; -import { validateFuncCoreToolsInstalled } from '../../funcCoreTools/validateFuncCoreToolsInstalled'; import { localize } from '../../localize'; import { getGlobalSetting, getWorkspaceSetting } from '../../vsCodeConfig/settings'; import type * as api from '../../vscode-azurefunctions.api'; @@ -19,7 +18,7 @@ import { FolderListStep } from './FolderListStep'; import { NewProjectLanguageStep } from './NewProjectLanguageStep'; import { OpenBehaviorStep } from './OpenBehaviorStep'; import { OpenFolderStep } from './OpenFolderStep'; -import { CreateDockerfileProjectStep } from './dockerfileSteps/CreateDockerfileProjectStep'; +import { ProjectTypeListStep } from './ProjectTypeListStep'; /** * @deprecated Use AzureFunctionsExtensionApi.createFunction instead @@ -54,15 +53,6 @@ export async function createNewProjectInternal(context: IActionContext, options: const version: string = options.version || getGlobalSetting(funcVersionSetting) || await tryGetLocalFuncVersion(context, undefined) || latestGAVersion; const projectTemplateKey: string | undefined = getGlobalSetting(projectTemplateKeySetting); const wizardContext: Partial & IActionContext = Object.assign(context, options, { language, version: tryParseFuncVersion(version), projectTemplateKey }); - const optionalExecuteStep = options.executeStep; - - if (optionalExecuteStep instanceof CreateDockerfileProjectStep) { - const message: string = localize('installFuncTools', 'You must have the Azure Functions Core Tools installed to run this command.'); - if (!await validateFuncCoreToolsInstalled(context, message)) { - throw new UserCancelledError('validateFuncCoreToolsInstalled'); - } - wizardContext.containerizedProject = true; - } if (options.folderPath) { FolderListStep.setProjectPath(wizardContext, options.folderPath); @@ -77,8 +67,8 @@ export async function createNewProjectInternal(context: IActionContext, options: const wizard: AzureWizard = new AzureWizard(wizardContext, { title: localize('createNewProject', 'Create new project'), - promptSteps: [new FolderListStep(), new NewProjectLanguageStep(options.templateId, options.functionSettings), new OpenBehaviorStep()], - executeSteps: optionalExecuteStep ? [optionalExecuteStep, new OpenFolderStep()] : [new OpenFolderStep()] + promptSteps: [new FolderListStep(), new ProjectTypeListStep(), new NewProjectLanguageStep(options.templateId, options.functionSettings), new OpenBehaviorStep()], + executeSteps: [new OpenFolderStep()] }); await wizard.prompt(); diff --git a/src/commands/deploy/FunctionAppListStep.ts b/src/commands/deploy/FunctionAppListStep.ts new file mode 100644 index 000000000..983ca552c --- /dev/null +++ b/src/commands/deploy/FunctionAppListStep.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { type Site } from "@azure/arm-appservice"; +import { parseAzureResourceGroupId, uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { AzureWizardPromptStep, createSubscriptionContext, nonNullProp, type AzureWizardExecuteStep, type IAzureQuickPickItem, type IWizardOptions } from "@microsoft/vscode-azext-utils"; +import * as vscode from 'vscode'; +import { projectLanguageSetting } from "../../constants"; +import { type ICreateFunctionAppContext } from "../../tree/SubscriptionTreeItem"; +import { createWebSiteClient } from "../../utils/azureClients"; +import { getWorkspaceSetting, getWorkspaceSettingFromAnyFolder } from "../../vsCodeConfig/settings"; +import { createCreateFunctionAppComponents } from "../createFunctionApp/createCreateFunctionAppComponents"; +import { type IFuncDeployContext } from "./deploy"; + +export class FunctionAppListStep extends AzureWizardPromptStep { + public async prompt(context: IFuncDeployContext): Promise { + context.site = (await context.ui.showQuickPick(this.getPicks(context), { placeHolder: vscode.l10n.t("Select a function app") })).data; + } + + public shouldPrompt(context: IFuncDeployContext): boolean { + return !context.site; + } + + private async getPicks(context: IFuncDeployContext): Promise[]> { + const client = await createWebSiteClient([context, createSubscriptionContext(nonNullProp(context, 'subscription'))]); + const sites = (await uiUtils.listAllIterator(client.webApps.list())); + const qp: IAzureQuickPickItem[] = sites.filter(s => !!s.kind?.includes('functionapp')).map(fa => { + return { + label: nonNullProp(fa, 'name'), + description: parseAzureResourceGroupId(nonNullProp(fa, 'id')).resourceGroup, + data: fa + } + }); + + qp.unshift({ label: '$(plus) Create new function app', description: '', data: undefined }); + return qp; + } + + public async getSubWizard(context: IFuncDeployContext): Promise | undefined> { + if (!context.site) { + const language: string | undefined = context.workspaceFolder ? getWorkspaceSetting(projectLanguageSetting, context.workspaceFolder) : getWorkspaceSettingFromAnyFolder(projectLanguageSetting); + context.telemetry.properties.projectLanguage = language; + + const { promptSteps, executeSteps } = await createCreateFunctionAppComponents(context as ICreateFunctionAppContext, + createSubscriptionContext(nonNullProp(context, 'subscription')), + language); + return { + // it's ugly, but we can cast because we know that this subwizard doesn't need to have the full IFuncDeployContext + promptSteps: promptSteps as unknown as AzureWizardPromptStep[], + executeSteps: executeSteps as unknown as AzureWizardExecuteStep[] + }; + } + + return; + } +} diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index 02dc81007..e06f1a2c7 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type SiteConfigResource } from '@azure/arm-appservice'; +import { type Site, type SiteConfigResource } from '@azure/arm-appservice'; import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths } from '@microsoft/vscode-azext-azureappservice'; import { DialogResponses, type ExecuteActivityContext, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import type * as vscode from 'vscode'; -import { CodeAction, ConnectionType, DurableBackend, ProjectLanguage, ScmType, deploySubpathSetting, functionFilter, hostFileName, remoteBuildSetting, type DurableBackendValues } from '../../constants'; +import { CodeAction, ConnectionType, DurableBackend, ProjectLanguage, ScmType, deploySubpathSetting, hostFileName, remoteBuildSetting, type DurableBackendValues } from '../../constants'; import { ext } from '../../extensionVariables'; import { addLocalFuncTelemetry } from '../../funcCoreTools/getLocalFuncCoreToolsVersion'; import { localize } from '../../localize'; import { ResolvedFunctionAppResource } from '../../tree/ResolvedFunctionAppResource'; import { type SlotTreeItem } from '../../tree/SlotTreeItem'; +import { type ICreateFunctionAppContext } from '../../tree/SubscriptionTreeItem'; import { createActivityContext } from '../../utils/activityUtils'; import { dotnetUtils } from '../../utils/dotnetUtils'; import { durableUtils } from '../../utils/durableUtils'; @@ -24,6 +26,7 @@ import { type ISetConnectionSettingContext } from '../appSettings/connectionSett import { validateEventHubsConnection } from '../appSettings/connectionSettings/eventHubs/validateEventHubsConnection'; import { validateSqlDbConnection } from '../appSettings/connectionSettings/sqlDatabase/validateSqlDbConnection'; import { tryGetFunctionProjectRoot } from '../createNewProject/verifyIsProject'; +import { getOrCreateFunctionApp } from './getOrCreateFunctionApp'; import { notifyDeployComplete } from './notifyDeployComplete'; import { runPreDeployTask } from './runPreDeployTask'; import { shouldValidateConnections } from './shouldValidateConnection'; @@ -31,7 +34,9 @@ import { showCoreToolsWarning } from './showCoreToolsWarning'; import { validateRemoteBuild } from './validateRemoteBuild'; import { verifyAppSettings } from './verifyAppSettings'; -export type IFuncDeployContext = IDeployContext & ISetConnectionSettingContext & ExecuteActivityContext; +// context that is used for deployment but since creation is an option in the deployment command, include ICreateFunctionAppContext +export type IFuncDeployContext = { site?: Site, subscription?: AzureSubscription } & + Partial & IDeployContext & ISetConnectionSettingContext & ExecuteActivityContext; export async function deployProductionSlot(context: IActionContext, target?: vscode.Uri | string | SlotTreeItem, functionAppId?: string | {}): Promise { await deploy(context, target, functionAppId); @@ -41,7 +46,7 @@ export async function deploySlot(context: IActionContext, target?: vscode.Uri | await deploy(context, target, functionAppId, new RegExp(ResolvedFunctionAppResource.pickSlotContextValue)); } -async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string | SlotTreeItem | undefined, arg2: string | {} | undefined, expectedContextValue?: string | RegExp): Promise { +async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string | SlotTreeItem | undefined, arg2: string | {} | undefined, _expectedContextValue?: string | RegExp): Promise { const deployPaths: IDeployPaths = await getDeployFsPath(actionContext, arg1); addLocalFuncTelemetry(actionContext, deployPaths.workspaceFolder.uri.fsPath); @@ -68,10 +73,9 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string | } } - const node: SlotTreeItem = await getDeployNode(context, ext.rgApi.tree, arg1, arg2, async () => ext.rgApi.pickAppResource(context, { - filter: functionFilter, - expectedChildContextValue: expectedContextValue - })); + const node: SlotTreeItem = await getDeployNode(context, ext.rgApi.tree, arg1, arg2, async () => { + return await getOrCreateFunctionApp(context) + }); const { language, languageModel, version } = await verifyInitForVSCode(context, context.effectiveDeployFsPath); diff --git a/src/commands/deploy/getOrCreateFunctionApp.ts b/src/commands/deploy/getOrCreateFunctionApp.ts new file mode 100644 index 000000000..9bc80cb86 --- /dev/null +++ b/src/commands/deploy/getOrCreateFunctionApp.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; +import { AzureWizard, nonNullProp, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; +import { l10n } from "vscode"; +import { ext } from "../../extensionVariables"; +import { localize } from "../../localize"; +import { ResolvedFunctionAppResource } from "../../tree/ResolvedFunctionAppResource"; +import { type SlotTreeItem } from "../../tree/SlotTreeItem"; +import { SubscriptionListStep } from "../SubscriptionListStep"; +import { type IFunctionAppWizardContext } from "../createFunctionApp/IFunctionAppWizardContext"; +import { FunctionAppListStep } from "./FunctionAppListStep"; +import { type IFuncDeployContext } from "./deploy"; + +export async function getOrCreateFunctionApp(context: IFuncDeployContext & Partial): Promise { + let node: SlotTreeItem | undefined; + + const promptSteps = [new SubscriptionListStep(), new FunctionAppListStep()]; + const title: string = l10n.t('Select Function App'); + const wizard: AzureWizard = new AzureWizard(context, { + promptSteps, + title + }); + + await wizard.prompt(); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node = context.site ? await ext.rgApi.tree.findTreeItem(context.site!.id!, context)! : undefined; + + if (!node) { + context.activityTitle = localize('functionAppCreateActivityTitle', 'Create Function App "{0}"', nonNullProp(context, 'newSiteName')) + await wizard.execute(); + + const resolved = new ResolvedFunctionAppResource(context as ISubscriptionContext, nonNullProp(context, 'site')); + await ext.rgApi.tree.refresh(context); + + node = await ext.rgApi.tree.findTreeItem(resolved.id, context); + context.isNewApp = true; + } + + if (!node) { + throw new Error(l10n.t('Could not find function app "{0}".', context.site?.name ?? '')); + } + + return node; +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 28fd13a9a..bbd20680d 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -32,8 +32,7 @@ import { copyFunctionUrl } from './copyFunctionUrl'; import { createChildNode } from './createChildNode'; import { createFunctionFromCommand } from './createFunction/createFunction'; import { createFunctionApp, createFunctionAppAdvanced } from './createFunctionApp/createFunctionApp'; -import { createNewProjectFromCommand, createNewProjectInternal } from './createNewProject/createNewProject'; -import { CreateDockerfileProjectStep } from './createNewProject/dockerfileSteps/CreateDockerfileProjectStep'; +import { createNewProjectFromCommand } from './createNewProject/createNewProject'; import { createSlot } from './createSlot'; import { deleteFunction } from './deleteFunction'; import { deleteFunctionApp } from './deleteFunctionApp'; @@ -95,14 +94,6 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping('azureFunctions.createFunctionApp', createFunctionApp); registerCommandWithTreeNodeUnwrapping('azureFunctions.createFunctionAppAdvanced', createFunctionAppAdvanced); registerCommand('azureFunctions.createNewProject', createNewProjectFromCommand); - registerCommandWithTreeNodeUnwrapping( - 'azureFunctions.createNewProjectWithDockerfile', - async (context: IActionContext) => - await createNewProjectInternal(context, { - executeStep: new CreateDockerfileProjectStep(), - languageFilter: /Python|C\#|(Java|Type)Script|PowerShell$/i, - }), - ); registerCommandWithTreeNodeUnwrapping('azureFunctions.createSlot', createSlot); registerCommandWithTreeNodeUnwrapping('azureFunctions.deleteFunction', deleteFunction); registerCommandWithTreeNodeUnwrapping('azureFunctions.deleteFunctionApp', deleteFunctionApp); @@ -112,6 +103,7 @@ export function registerCommands(): void { ); registerCommandWithTreeNodeUnwrapping('azureFunctions.disableFunction', disableFunction); registerCommandWithTreeNodeUnwrapping('azureFunctions.deploy', deployProductionSlot); + registerCommandWithTreeNodeUnwrapping('azureFunctions.deployProject', deployProductionSlot); registerCommandWithTreeNodeUnwrapping('azureFunctions.deploySlot', deploySlot); registerCommandWithTreeNodeUnwrapping('azureFunctions.disconnectRepo', disconnectRepo); registerCommandWithTreeNodeUnwrapping('azureFunctions.enableFunction', enableFunction); diff --git a/src/getExtensionApi.ts b/src/getExtensionApi.ts index 5010b6c74..388f9df7e 100644 --- a/src/getExtensionApi.ts +++ b/src/getExtensionApi.ts @@ -12,7 +12,7 @@ import type * as acaApi from "./vscode-azurecontainerapps.api"; export async function getResourceGroupsApi(): Promise { const rgApiProvider = await apiUtils.getExtensionExports('ms-azuretools.vscode-azureresourcegroups'); if (rgApiProvider) { - return rgApiProvider.getApi('0.0.1'); + return rgApiProvider.getApi('^0.0.1'); } else { throw new Error(localize('noResourceGroupExt', 'Could not find the Azure Resource Groups extension')); } diff --git a/src/tree/AzureAccountTreeItemWithProjects.ts b/src/tree/AzureAccountTreeItemWithProjects.ts index 3ee604fe0..a45585da5 100644 --- a/src/tree/AzureAccountTreeItemWithProjects.ts +++ b/src/tree/AzureAccountTreeItemWithProjects.ts @@ -20,6 +20,7 @@ import { LocalProjectTreeItemBase } from './localProject/LocalProjectTreeItemBas import { createRefreshFileWatcher } from './localProject/createRefreshFileWatcher'; import { isLocalProjectCV, isProjectCV, isRemoteProjectCV } from './projectContextValues'; +// TODO: Remove this file. Only the long running tests rely on it (which we need to redo) export class AzureAccountTreeItemWithProjects extends AzureAccountTreeItemBase { private _projectDisposables: Disposable[] = []; @@ -84,7 +85,7 @@ export class AzureAccountTreeItemWithProjects extends AzureAccountTreeItemBase { if (!hasLocalProject && children.length > 0 && children[0] instanceof GenericTreeItem) { const ti: GenericTreeItem = new GenericTreeItem(this, { label: localize('createNewProject', 'Create New Project...'), - commandId: 'azureFunctions.createNewProject', + commandId: 'azureFunctions.createFunction', contextValue: 'createNewProject', iconPath: treeUtils.getThemedIconPath('CreateNewProject') }); diff --git a/src/tree/SubscriptionTreeItem.ts b/src/tree/SubscriptionTreeItem.ts index 8ccfe391f..f0e991968 100644 --- a/src/tree/SubscriptionTreeItem.ts +++ b/src/tree/SubscriptionTreeItem.ts @@ -4,27 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { type Site, type WebSiteManagementClient } from '@azure/arm-appservice'; -import { AppInsightsCreateStep, AppInsightsListStep, AppKind, AppServicePlanCreateStep, AppServicePlanListStep, CustomLocationListStep, LogAnalyticsCreateStep, SiteNameStep, WebsiteOS, type IAppServiceWizardContext } from '@microsoft/vscode-azext-azureappservice'; -import { LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, StorageAccountCreateStep, StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication, SubscriptionTreeItemBase, uiUtils, type INewStorageAccountDefaults } from '@microsoft/vscode-azext-azureutils'; -import { AzureWizard, parseError, type AzExtTreeItem, type AzureWizardExecuteStep, type AzureWizardPromptStep, type IActionContext, type ICreateChildImplContext } from '@microsoft/vscode-azext-utils'; +import { type IAppServiceWizardContext } from '@microsoft/vscode-azext-azureappservice'; +import { SubscriptionTreeItemBase, uiUtils } from '@microsoft/vscode-azext-azureutils'; +import { AzureWizard, parseError, type AzExtTreeItem, type IActionContext, type ICreateChildImplContext } from '@microsoft/vscode-azext-utils'; import { type WorkspaceFolder } from 'vscode'; -import { FuncVersion, latestGAVersion, tryParseFuncVersion } from '../FuncVersion'; -import { FunctionAppCreateStep } from '../commands/createFunctionApp/FunctionAppCreateStep'; -import { FunctionAppHostingPlanStep, setConsumptionPlanProperties } from '../commands/createFunctionApp/FunctionAppHostingPlanStep'; -import { type IFunctionAppWizardContext } from '../commands/createFunctionApp/IFunctionAppWizardContext'; -import { ContainerizedFunctionAppCreateStep } from '../commands/createFunctionApp/containerImage/ContainerizedFunctionAppCreateStep'; -import { DeployWorkspaceProjectStep } from '../commands/createFunctionApp/containerImage/DeployWorkspaceProjectStep'; -import { detectDockerfile } from '../commands/createFunctionApp/containerImage/detectDockerfile'; -import { FunctionAppStackStep } from '../commands/createFunctionApp/stacks/FunctionAppStackStep'; -import { funcVersionSetting, projectLanguageSetting } from '../constants'; +import { createCreateFunctionAppComponents } from '../commands/createFunctionApp/createCreateFunctionAppComponents'; +import { projectLanguageSetting } from '../constants'; import { ext } from '../extensionVariables'; -import { tryGetLocalFuncVersion } from '../funcCoreTools/tryGetLocalFuncVersion'; import { localize } from "../localize"; -import { createActivityContext } from '../utils/activityUtils'; import { registerProviders } from '../utils/azure'; import { createWebSiteClient } from '../utils/azureClients'; import { nonNullProp } from '../utils/nonNull'; -import { getRootFunctionsWorkerRuntime, getWorkspaceSetting, getWorkspaceSettingFromAnyFolder } from '../vsCodeConfig/settings'; +import { getWorkspaceSetting, getWorkspaceSettingFromAnyFolder } from '../vsCodeConfig/settings'; import { type DeployWorkspaceProjectResults } from '../vscode-azurecontainerapps.api'; import { ResolvedFunctionAppResource } from './ResolvedFunctionAppResource'; import { SlotTreeItem } from './SlotTreeItem'; @@ -89,84 +80,13 @@ export class SubscriptionTreeItem extends SubscriptionTreeItemBase { } public static async createChild(context: ICreateFunctionAppContext, subscription: SubscriptionTreeItem): Promise { - const version: FuncVersion = await getDefaultFuncVersion(context); - context.telemetry.properties.projectRuntime = version; - const language: string | undefined = context.workspaceFolder ? getWorkspaceSetting(projectLanguageSetting, context.workspaceFolder) : getWorkspaceSettingFromAnyFolder(projectLanguageSetting); - context.telemetry.properties.projectLanguage = language; - + const language: string | undefined = context.workspaceFolder ? + getWorkspaceSetting(projectLanguageSetting, context.workspaceFolder) : + getWorkspaceSettingFromAnyFolder(projectLanguageSetting); // Ensure all the providers are registered before const registerProvidersTask = registerProviders(context, subscription); - - const wizardContext: IFunctionAppWizardContext = Object.assign(context, subscription.subscription, { - newSiteKind: AppKind.functionapp, - resourceGroupDeferLocationStep: true, - version, - language, - ...(await createActivityContext()) - }); - - const promptSteps: AzureWizardPromptStep[] = []; - const executeSteps: AzureWizardExecuteStep[] = []; - - const storageAccountCreateOptions: INewStorageAccountDefaults = { - kind: StorageAccountKind.Storage, - performance: StorageAccountPerformance.Standard, - replication: StorageAccountReplication.LRS - }; - - await detectDockerfile(context); - - promptSteps.push(new SiteNameStep(context.dockerfilePath ? "containerizedFunctionApp" : "functionApp")); - - if (context.dockerfilePath) { - const containerizedfunctionAppWizard = await createContainerizedFunctionAppWizard(); - promptSteps.push(...containerizedfunctionAppWizard.promptSteps); - executeSteps.push(...containerizedfunctionAppWizard.executeSteps); - } else { - const functionAppWizard = await createFunctionAppWizard(wizardContext); - promptSteps.push(...functionAppWizard.promptSteps); - executeSteps.push(...functionAppWizard.executeSteps); - } - - if (!wizardContext.advancedCreation) { - LocationListStep.addStep(wizardContext, promptSteps); - wizardContext.useConsumptionPlan = true; - wizardContext.stackFilter = getRootFunctionsWorkerRuntime(wizardContext.language); - executeSteps.push(new ResourceGroupCreateStep()); - executeSteps.push(new StorageAccountCreateStep(storageAccountCreateOptions)); - executeSteps.push(new AppInsightsCreateStep()); - if (!context.dockerfilePath) { - executeSteps.push(new AppServicePlanCreateStep()); - executeSteps.push(new LogAnalyticsCreateStep()); - } - } else { - promptSteps.push(new ResourceGroupListStep()); - CustomLocationListStep.addStep(wizardContext, promptSteps); - promptSteps.push(new StorageAccountListStep( - storageAccountCreateOptions, - { - // The account type must support blobs, queues, and tables. - // See: https://aka.ms/Cfqnrc - kind: [ - // Blob-only accounts don't support queues and tables - StorageAccountKind.BlobStorage - ], - performance: [ - // Premium performance accounts don't support queues and tables - StorageAccountPerformance.Premium - ], - learnMoreLink: 'https://aka.ms/Cfqnrc' - } - )); - promptSteps.push(new AppInsightsListStep()); - } - - - const storageProvider = 'Microsoft.Storage'; - LocationListStep.addProviderForFiltering(wizardContext, storageProvider, 'storageAccounts'); - + const { wizardContext, promptSteps, executeSteps } = await createCreateFunctionAppComponents(context, subscription.subscription, language) const title: string = localize('functionAppCreatingTitle', 'Create new Function App in Azure'); - const wizard: AzureWizard = new AzureWizard(wizardContext, { promptSteps, executeSteps, @@ -178,17 +98,7 @@ export class SubscriptionTreeItem extends SubscriptionTreeItemBase { await wizard.prompt(); // if the providers aren't registered yet, await it here because it is required by this point await registerProvidersTask; - if (!context.advancedCreation) { - const newName: string | undefined = await wizardContext.relatedNameTask; - if (!newName) { - throw new Error(localize('noUniqueName', 'Failed to generate unique name for resources. Use advanced creation to manually enter resource names.')); - } - wizardContext.newResourceGroupName = context.newResourceGroupName || newName; - setConsumptionPlanProperties(wizardContext); - wizardContext.newStorageAccountName = newName; - wizardContext.newAppInsightsName = newName; - } - + // move this into create function app step wizardContext.activityTitle = localize('functionAppCreateActivityTitle', 'Create Function App "{0}"', nonNullProp(wizardContext, 'newSiteName')) await wizard.execute(); @@ -210,56 +120,3 @@ export class SubscriptionTreeItem extends SubscriptionTreeItemBase { return !isProjectCV(contextValue) || isRemoteProjectCV(contextValue); } } - -async function getDefaultFuncVersion(context: ICreateFunctionAppContext): Promise { - const settingValue: string | undefined = context.workspaceFolder ? getWorkspaceSetting(funcVersionSetting, context.workspaceFolder) : getWorkspaceSettingFromAnyFolder(funcVersionSetting); - // Try to get VS Code setting for version (aka if they have a project open) - let version: FuncVersion | undefined = tryParseFuncVersion(settingValue); - context.telemetry.properties.runtimeSource = 'VSCodeSetting'; - - if (version === undefined) { - // Try to get the version that matches their local func cli - version = await tryGetLocalFuncVersion(context, undefined); - context.telemetry.properties.runtimeSource = 'LocalFuncCli'; - } - - if (version === undefined) { - version = latestGAVersion; - context.telemetry.properties.runtimeSource = 'Backup'; - } - - return version; -} - -async function createFunctionAppWizard(wizardContext: IFunctionAppWizardContext): Promise<{ promptSteps: AzureWizardPromptStep[], executeSteps: AzureWizardExecuteStep[] }> { - const promptSteps: AzureWizardPromptStep[] = []; - const executeSteps: AzureWizardExecuteStep[] = []; - - if (wizardContext.advancedCreation) { - promptSteps.push(new FunctionAppHostingPlanStep()); - } - - promptSteps.push(new FunctionAppStackStep()); - - if (wizardContext.advancedCreation) { - promptSteps.push(new AppServicePlanListStep()) - } - - if (wizardContext.version === FuncVersion.v1) { // v1 doesn't support linux - wizardContext.newSiteOS = WebsiteOS.windows; - } - - executeSteps.push(new FunctionAppCreateStep()); - - return { promptSteps, executeSteps }; -} - -async function createContainerizedFunctionAppWizard(): Promise<{ promptSteps: AzureWizardPromptStep[], executeSteps: AzureWizardExecuteStep[] }> { - const promptSteps: AzureWizardPromptStep[] = []; - const executeSteps: AzureWizardExecuteStep[] = []; - - executeSteps.push(new DeployWorkspaceProjectStep()); - executeSteps.push(new ContainerizedFunctionAppCreateStep()); - - return { promptSteps, executeSteps }; -} diff --git a/src/tree/localProject/InitLocalProjectTreeItem.ts b/src/tree/localProject/InitLocalProjectTreeItem.ts index 8043a652e..3d1847ac0 100644 --- a/src/tree/localProject/InitLocalProjectTreeItem.ts +++ b/src/tree/localProject/InitLocalProjectTreeItem.ts @@ -27,7 +27,7 @@ export class InitLocalProjectTreeItem extends LocalProjectTreeItemBase { public async loadMoreChildrenImpl(_clearCache: boolean): Promise { const ti: GenericTreeItem = new GenericTreeItem(this, { contextValue: 'initProject', - label: localize('initProject', 'Initialize Project for Use with VS Code...'), + label: localize('initProject', 'Initialize project for use with VS Code...'), commandId: 'azureFunctions.initProjectForVSCode', iconPath: new ThemeIcon('warning') }); diff --git a/test/addBinding.test.ts b/test/addBinding.test.ts index 6f4bf8931..8a484b4e2 100644 --- a/test/addBinding.test.ts +++ b/test/addBinding.test.ts @@ -20,7 +20,7 @@ suite('Add Binding', () => { await cleanTestWorkspace(); const testWorkspacePath = getTestWorkspaceFolder(); await runWithTestActionContext('createNewProject', async (context) => { - await context.ui.runWithInputs([testWorkspacePath, ProjectLanguage.JavaScript, 'Model V3', /http\s*trigger/i, functionName, 'Anonymous'], async () => { + await context.ui.runWithInputs([testWorkspacePath, 'Create default function project', ProjectLanguage.JavaScript, 'Model V3', /http\s*trigger/i, functionName, 'Anonymous'], async () => { await createNewProjectInternal(context, {}); }); }) diff --git a/test/project/createNewProject.test.ts b/test/project/createNewProject.test.ts index ef1fbb2b3..107ecbe53 100644 --- a/test/project/createNewProject.test.ts +++ b/test/project/createNewProject.test.ts @@ -16,19 +16,19 @@ interface CreateProjectTestCase extends ICreateProjectTestOptions { const testCases: CreateProjectTestCase[] = [ { ...getCSharpValidateOptions('netcoreapp2.1', FuncVersion.v2) }, - { ...getCSharpValidateOptions('netcoreapp3.1', FuncVersion.v3), inputs: [/3/], description: 'netcoreapp3.1' }, - { ...getCSharpValidateOptions('net6.0', FuncVersion.v4), inputs: [/6/], description: 'net6.0' }, - { ...getCSharpValidateOptions('net6.0', FuncVersion.v4), inputs: [/6.*isolated/i], description: 'net6.0 isolated' }, - { ...getCSharpValidateOptions('net7.0', FuncVersion.v4), inputs: [/7.*isolated/i], description: 'net7.0 isolated' }, + { ...getCSharpValidateOptions('netcoreapp3.1', FuncVersion.v3), inputs: [TestInput.UseDefaultValue, /3/], description: 'netcoreapp3.1' }, + { ...getCSharpValidateOptions('net6.0', FuncVersion.v4), inputs: [TestInput.UseDefaultValue, /6/], description: 'net6.0' }, + { ...getCSharpValidateOptions('net6.0', FuncVersion.v4), inputs: [TestInput.UseDefaultValue, /6.*isolated/i], description: 'net6.0 isolated' }, + { ...getCSharpValidateOptions('net7.0', FuncVersion.v4), inputs: [TestInput.UseDefaultValue, /7.*isolated/i], description: 'net7.0 isolated' }, { ...getFSharpValidateOptions('netcoreapp2.1', FuncVersion.v2), isHiddenLanguage: true }, - { ...getFSharpValidateOptions('netcoreapp3.1', FuncVersion.v3), inputs: [/3/], isHiddenLanguage: true }, + { ...getFSharpValidateOptions('netcoreapp3.1', FuncVersion.v3), inputs: [TestInput.UseDefaultValue, /3/], isHiddenLanguage: true }, ]; // Test cases that are the same for both v2 and v3 for (const version of [FuncVersion.v2, FuncVersion.v3, FuncVersion.v4]) { testCases.push( - { ...getJavaScriptValidateOptions(true /* hasPackageJson */, version), inputs: ['Model V3'] }, - { ...getTypeScriptValidateOptions({ version }), inputs: ['Model V3'] }, + { ...getJavaScriptValidateOptions(true /* hasPackageJson */, version), inputs: [TestInput.UseDefaultValue, 'Model V3'] }, + { ...getTypeScriptValidateOptions({ version }), inputs: [TestInput.UseDefaultValue, 'Model V3'] }, { ...getPowerShellValidateOptions(version) }, { ...getDotnetScriptValidateOptions(ProjectLanguage.CSharpScript, version), isHiddenLanguage: true }, { ...getDotnetScriptValidateOptions(ProjectLanguage.FSharpScript, version), isHiddenLanguage: true }, @@ -37,11 +37,11 @@ for (const version of [FuncVersion.v2, FuncVersion.v3, FuncVersion.v4]) { // test python v1 model testCases.push({ ...getPythonValidateOptions('.venv', version), - inputs: [/Model V1/i, TestInput.UseDefaultValue] + inputs: [TestInput.UseDefaultValue, /Model V1/i, TestInput.UseDefaultValue] }); const appName: string = 'javaApp'; - const javaBaseInputs: (TestInput | string | RegExp)[] = [TestInput.UseDefaultValue, TestInput.UseDefaultValue, TestInput.UseDefaultValue, TestInput.UseDefaultValue, appName]; + const javaBaseInputs: (TestInput | string | RegExp)[] = [TestInput.UseDefaultValue, TestInput.UseDefaultValue, TestInput.UseDefaultValue, TestInput.UseDefaultValue, TestInput.UseDefaultValue, appName]; if (version !== FuncVersion.v2) { // v2 doesn't support picking a java version javaBaseInputs.unshift(/8/); }