Skip to content

Commit 833184e

Browse files
authored
Group templates by filter rather than filtering it out (#4773)
* Group templates by filter rather than filtering it out * Readd new property * Remove telemetry
1 parent 3c2cd0b commit 833184e

File tree

10 files changed

+52
-83
lines changed

10 files changed

+52
-83
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@
10831083
"title": "Azure Functions",
10841084
"properties": {
10851085
"azureFunctions.templateFilter": {
1086+
"deprecationMessage": "This setting is no longer used to filter templates, but is being left to explain what the filter enumerations mean.",
10861087
"scope": "resource",
10871088
"type": "string",
10881089
"default": "Verified",

src/commands/createFunction/FunctionListStep.ts

Lines changed: 24 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { AzureWizardPromptStep, type IActionContext, type IAzureQuickPickItem, type IAzureQuickPickOptions, type IWizardOptions } from '@microsoft/vscode-azext-utils';
6+
import { AzureWizardPromptStep, type IAzureQuickPickItem, type IWizardOptions } from '@microsoft/vscode-azext-utils';
77
import * as escape from 'escape-string-regexp';
88
import { type FuncVersion } from '../../FuncVersion';
9-
import { JavaBuildTool, ProjectLanguage, TemplateFilter, templateFilterSetting } from '../../constants';
9+
import { JavaBuildTool, ProjectLanguage } from '../../constants';
1010
import { ext } from '../../extensionVariables';
1111
import { localize } from '../../localize';
1212
import { type FunctionTemplateBase, type IFunctionTemplate } from '../../templates/IFunctionTemplate';
1313
import { TemplateSchemaVersion } from '../../templates/TemplateProviderBase';
1414
import { durableUtils } from '../../utils/durableUtils';
1515
import { nonNullProp } from '../../utils/nonNull';
1616
import { isNodeV4Plus, isPythonV2Plus, nodeV4Suffix } from '../../utils/programmingModelUtils';
17-
import { getWorkspaceSetting, updateWorkspaceSetting } from '../../vsCodeConfig/settings';
17+
import { getWorkspaceSetting } from '../../vsCodeConfig/settings';
1818
import { FunctionSubWizard } from './FunctionSubWizard';
1919
import { type IFunctionWizardContext } from './IFunctionWizardContext';
2020
import { JobsListStep } from './JobsListStep';
@@ -39,7 +39,7 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
3939
const language: ProjectLanguage = nonNullProp(context, 'language');
4040
const version: FuncVersion = nonNullProp(context, 'version');
4141
const templateProvider = ext.templateProvider.get(context);
42-
const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, context.projectPath, language, context.languageModel, version, TemplateFilter.All, context.projectTemplateKey);
42+
const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, context.projectPath, language, context.languageModel, version, context.projectTemplateKey);
4343
const foundTemplate: FunctionTemplateBase | undefined = templates.find((t: FunctionTemplateBase) => {
4444
if (this._options.templateId) {
4545
const actualId: string = t.id.toLowerCase();
@@ -83,10 +83,6 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
8383
}
8484

8585
public async prompt(context: IFunctionWizardContext): Promise<void> {
86-
/* v2 schema doesn't have a template filter setting */
87-
let templateFilter: TemplateFilter = context.templateSchemaVersion === TemplateSchemaVersion.v2 ? TemplateFilter.All :
88-
getWorkspaceSetting<TemplateFilter>(templateFilterSetting, context.projectPath) || TemplateFilter.Verified;
89-
9086
const templateProvider = ext.templateProvider.get(context);
9187
while (!context.functionTemplate) {
9288
let placeHolder: string = this._isProjectWizard ?
@@ -97,17 +93,13 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
9793
placeHolder += localize('templateSource', ' (Template source: "{0}")', templateProvider.templateSource)
9894
}
9995

100-
const result: FunctionTemplateBase | TemplatePromptResult = (await context.ui.showQuickPick(this.getPicks(context, templateFilter), { placeHolder })).data;
96+
const result: FunctionTemplateBase | TemplatePromptResult =
97+
(await context.ui.showQuickPick(this.getPicks(context),
98+
{ placeHolder, enableGrouping: true })).data;
99+
101100
if (result === 'skipForNow') {
102101
context.telemetry.properties.templateId = 'skipForNow';
103102
break;
104-
} else if (result === 'changeFilter') {
105-
templateFilter = await promptForTemplateFilter(context);
106-
// can only update setting if it's open in a workspace
107-
if (!this._isProjectWizard || context.openBehavior === 'AlreadyOpen') {
108-
await updateWorkspaceSetting(templateFilterSetting, templateFilter, context.projectPath);
109-
}
110-
context.telemetry.properties.changedFilter = 'true';
111103
} else if (result === 'openAPI') {
112104
context.generateFromOpenAPI = true;
113105
break;
@@ -117,8 +109,6 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
117109
} else {
118110
context.functionTemplate = result;
119111
}
120-
121-
context.telemetry.properties.templateFilter = templateFilter;
122112
}
123113
}
124114

@@ -128,18 +118,18 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
128118
context.language !== ProjectLanguage.SelfHostedMCPServer;
129119
}
130120

131-
private async getPicks(context: IFunctionWizardContext, templateFilter: TemplateFilter): Promise<IAzureQuickPickItem<FunctionTemplateBase | TemplatePromptResult>[]> {
121+
private async getPicks(context: IFunctionWizardContext): Promise<IAzureQuickPickItem<FunctionTemplateBase | TemplatePromptResult>[]> {
132122
const language: ProjectLanguage = nonNullProp(context, 'language');
133123
const languageModel = context.languageModel;
134124
const version: FuncVersion = nonNullProp(context, 'version');
135125
const templateProvider = ext.templateProvider.get(context);
136126

137-
const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, context.projectPath, language, context.languageModel, version, templateFilter, context.projectTemplateKey);
127+
const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, context.projectPath, language, context.languageModel, version, context.projectTemplateKey);
138128
context.telemetry.measurements.templateCount = templates.length;
139129
const picks: IAzureQuickPickItem<FunctionTemplateBase | TemplatePromptResult>[] = templates
140130
.filter((t) => !(doesTemplateRequireExistingStorageSetup(t.id, language) && !context.hasDurableStorage))
141-
.sort((a, b) => sortTemplates(a, b, templateFilter))
142-
.map(t => { return { label: t.name, data: t }; });
131+
.sort((a, b) => sortTemplates(a, b))
132+
.map(t => { return { label: t.name, data: t, group: t.templateFilter }; });
143133

144134
if (this._isProjectWizard) {
145135
picks.unshift({
@@ -167,15 +157,6 @@ export class FunctionListStep extends AzureWizardPromptStep<IFunctionWizardConte
167157
suppressPersistence: true
168158
});
169159
}
170-
if (context.templateSchemaVersion !== TemplateSchemaVersion.v2) {
171-
// don't offer template filter for v2 schema
172-
picks.push({
173-
label: localize('selectFilter', '$(gear) Change template filter'),
174-
description: localize('currentFilter', 'Current: {0}', templateFilter),
175-
data: 'changeFilter',
176-
suppressPersistence: true
177-
});
178-
}
179160

180161
if (getWorkspaceSetting<boolean>('showReloadTemplates')) {
181162
picks.push({
@@ -195,18 +176,7 @@ interface IFunctionListStepOptions {
195176
functionSettings: { [key: string]: string | undefined } | undefined;
196177
}
197178

198-
type TemplatePromptResult = 'changeFilter' | 'skipForNow' | 'openAPI' | 'reloadTemplates';
199-
200-
async function promptForTemplateFilter(context: IActionContext): Promise<TemplateFilter> {
201-
const picks: IAzureQuickPickItem<TemplateFilter>[] = [
202-
{ label: TemplateFilter.Verified, description: localize('verifiedDescription', '(Subset of "Core" that has been verified in VS Code)'), data: TemplateFilter.Verified },
203-
{ label: TemplateFilter.Core, data: TemplateFilter.Core },
204-
{ label: TemplateFilter.All, data: TemplateFilter.All }
205-
];
206-
207-
const options: IAzureQuickPickOptions = { suppressPersistence: true, placeHolder: localize('selectFilter', 'Select a template filter') };
208-
return (await context.ui.showQuickPick(picks, options)).data;
209-
}
179+
type TemplatePromptResult = 'skipForNow' | 'openAPI' | 'reloadTemplates';
210180

211181
// Todo: https://github.com/microsoft/vscode-azurefunctions/issues/3529
212182
// Identify and filter out Durable Function templates requiring a pre-existing storage setup
@@ -228,21 +198,17 @@ function doesTemplateRequireExistingStorageSetup(templateId: string, language?:
228198
* If templateFilter is verified, puts HttpTrigger/TimerTrigger at the top since they're the most popular
229199
* Otherwise sort alphabetically
230200
*/
231-
function sortTemplates(a: FunctionTemplateBase, b: FunctionTemplateBase, templateFilter: TemplateFilter): number {
232-
if (templateFilter === TemplateFilter.Verified) {
233-
function getPriority(id: string): number {
234-
if (/\bhttptrigger\b/i.test(id)) { // Plain http trigger
235-
return 1;
236-
} else if (/\bhttptrigger/i.test(id)) { // Http trigger with any extra pizazz
237-
return 2;
238-
} else if (/\btimertrigger\b/i.test(id)) {
239-
return 3;
240-
} else {
241-
return 4;
242-
}
201+
function sortTemplates(a: FunctionTemplateBase, b: FunctionTemplateBase): number {
202+
function getPriority(id: string): number {
203+
if (/\bhttptrigger\b/i.test(id)) { // Plain http trigger
204+
return 1;
205+
} else if (/\bhttptrigger/i.test(id)) { // Http trigger with any extra pizazz
206+
return 2;
207+
} else if (/\btimertrigger\b/i.test(id)) {
208+
return 3;
209+
} else {
210+
return a.name.localeCompare(b.name) === -1 ? 4 : 5;
243211
}
244-
return getPriority(a.id) - getPriority(b.id);
245212
}
246-
247-
return a.name.localeCompare(b.name);
213+
return getPriority(a.id) - getPriority(b.id);
248214
}

src/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export const projectLanguageSetting: string = 'projectLanguage';
1111
export const projectLanguageModelSetting: string = 'projectLanguageModel';
1212
export const funcVersionSetting: string = 'projectRuntime'; // Using this name for the sake of backwards compatability even though it's not the most accurate
1313
export const projectSubpathSetting: string = 'projectSubpath';
14-
export const templateFilterSetting: string = 'templateFilter';
1514
export const deploySubpathSetting: string = 'deploySubpath';
1615
export const templateVersionSetting: string = 'templateVersion';
1716
export const preDeployTaskSetting: string = 'preDeployTask';

src/templates/CentralTemplateProvider.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,20 @@ export class CentralTemplateProvider implements Disposable {
8383
}
8484

8585
/* Ignored by the v2 schema */
86-
public async getFunctionTemplates(context: IActionContext, projectPath: string | undefined, language: ProjectLanguage, languageModel: number | undefined, version: FuncVersion, templateFilter: TemplateFilter, projectTemplateKey: string | undefined): Promise<FunctionTemplateBase[]> {
86+
public async getFunctionTemplates(context: IActionContext, projectPath: string | undefined, language: ProjectLanguage, languageModel: number | undefined, version: FuncVersion, projectTemplateKey: string | undefined): Promise<FunctionTemplateBase[]> {
8787
const templates: ITemplates = await this.getTemplates(context, projectPath, language, languageModel, version, projectTemplateKey);
88-
switch (templateFilter) {
89-
case TemplateFilter.All:
90-
return templates.functionTemplates;
91-
case TemplateFilter.Core:
92-
return templates.functionTemplates.filter((t: IFunctionTemplate) => t.categories.find((c: TemplateCategory) => c === TemplateCategory.Core) !== undefined);
93-
case TemplateFilter.Verified:
94-
default:
95-
const verifiedTemplateIds = getScriptVerifiedTemplateIds(version).concat(getDotnetVerifiedTemplateIds(version)).concat(getJavaVerifiedTemplateIds().concat(getBallerinaVerifiedTemplateIds()));
96-
return templates.functionTemplates.filter((t: IFunctionTemplate) => verifiedTemplateIds.find(vt => typeof vt === 'string' ? vt === t.id : vt.test(t.id)));
88+
for (const template of templates.functionTemplates) {
89+
// by default, all templates will be categorized as 'All'
90+
template.templateFilter = TemplateFilter.All;
91+
const verifiedTemplateIds = getScriptVerifiedTemplateIds(version).concat(getDotnetVerifiedTemplateIds(version)).concat(getJavaVerifiedTemplateIds().concat(getBallerinaVerifiedTemplateIds()));
92+
if (verifiedTemplateIds.find(vt => typeof vt === 'string' ? vt === template.id : vt.test(template.id))) {
93+
template.templateFilter = TemplateFilter.Verified;
94+
} else if ((template as IFunctionTemplate).categories.find((c: TemplateCategory) => c === TemplateCategory.Core) !== undefined) {
95+
template.templateFilter = TemplateFilter.Core;
96+
}
9797
}
98+
99+
return templates.functionTemplates;
98100
}
99101

100102
public async clearTemplateCache(context: IActionContext, projectPath: string | undefined, language: ProjectLanguage, languageModel: number | undefined, version: FuncVersion): Promise<void> {
@@ -117,7 +119,7 @@ export class CentralTemplateProvider implements Disposable {
117119

118120
public async tryGetSampleData(context: IActionContext, version: FuncVersion, triggerBindingType: string): Promise<string | undefined> {
119121
try {
120-
const templates: IScriptFunctionTemplate[] = <IScriptFunctionTemplate[]>await this.getFunctionTemplates(context, undefined, ProjectLanguage.JavaScript, undefined, version, TemplateFilter.All, undefined);
122+
const templates: IScriptFunctionTemplate[] = <IScriptFunctionTemplate[]>await this.getFunctionTemplates(context, undefined, ProjectLanguage.JavaScript, undefined, version, undefined);
121123
const template: IScriptFunctionTemplate | undefined = templates.find(t => t.functionJson.triggerBinding?.type?.toLowerCase() === triggerBindingType.toLowerCase());
122124
return template?.templateFiles['sample.dat'];
123125
} catch {

src/templates/IFunctionTemplate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { type ProjectLanguage } from '../constants';
6+
import { type ProjectLanguage, type TemplateFilter } from '../constants';
77
import { type IBindingSetting } from './IBindingTemplate';
88
import { type TemplateSchemaVersion } from './TemplateProviderBase';
99
import { type ParsedJob, type RawTemplateV2 } from './script/parseScriptTemplatesV2';
@@ -46,4 +46,5 @@ export interface FunctionTemplateBase {
4646
isTimerTrigger: boolean;
4747
isMcpTrigger: boolean;
4848
templateSchemaVersion: TemplateSchemaVersion
49+
templateFilter?: TemplateFilter; // defaults to All
4950
}

src/templates/dotnet/parseDotnetTemplates.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ function parseDotnetTemplate(rawTemplate: IRawTemplate): IFunctionTemplate {
8686
userPromptedSettings: userPromptedSettings,
8787
categories: [TemplateCategory.Core], // Dotnet templates do not have category information, so display all templates as if they are in the 'core' category
8888
isDynamicConcurrent: (rawTemplate.Identity.includes('ServiceBusQueueTrigger') || rawTemplate.Identity.includes('BlobTrigger') || rawTemplate.Identity.includes('QueueTrigger')) ? true : false,
89-
templateSchemaVersion: TemplateSchemaVersion.v1
89+
templateSchemaVersion: TemplateSchemaVersion.v1,
90+
templateFilter: TemplateFilter.All
9091
};
9192
}
9293

@@ -123,7 +124,7 @@ async function copyCSharpSettingsFromJS(csharpTemplates: IFunctionTemplate[], ve
123124
jsContext.telemetry.properties.isActivationEvent = 'true';
124125

125126
const templateProvider = ext.templateProvider.get(jsContext);
126-
const jsTemplates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(jsContext, undefined, ProjectLanguage.JavaScript, undefined, version, TemplateFilter.All, undefined);
127+
const jsTemplates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(jsContext, undefined, ProjectLanguage.JavaScript, undefined, version, undefined);
127128
for (const csharpTemplate of csharpTemplates) {
128129
assertTemplateIsV1(csharpTemplate);
129130
csharpTemplate.templateSchemaVersion = TemplateSchemaVersion.v1;

src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as path from 'path';
88
import type * as vscode from 'vscode';
99
import { tryGetFunctionProjectRoot } from '../commands/createNewProject/verifyIsProject';
1010
import { initProjectForVSCode } from '../commands/initProjectForVSCode/initProjectForVSCode';
11-
import { funcVersionSetting, ProjectLanguage, projectLanguageModelSetting, projectLanguageSetting, TemplateFilter } from '../constants';
11+
import { funcVersionSetting, ProjectLanguage, projectLanguageModelSetting, projectLanguageSetting } from '../constants';
1212
import { ext } from '../extensionVariables';
1313
import { tryParseFuncVersion, type FuncVersion } from '../FuncVersion';
1414
import { localize } from '../localize';
@@ -38,7 +38,7 @@ export async function verifyVSCodeConfigOnActivate(context: IActionContext, fold
3838
templatesContext.telemetry.properties.isActivationEvent = 'true';
3939
templatesContext.errorHandling.suppressDisplay = true;
4040
const templateProvider = ext.templateProvider.get(templatesContext);
41-
await templateProvider.getFunctionTemplates(templatesContext, projectPath, language, languageModel, version, TemplateFilter.Verified, undefined);
41+
await templateProvider.getFunctionTemplates(templatesContext, projectPath, language, languageModel, version, undefined);
4242
});
4343

4444
let isDotnet: boolean = false;

0 commit comments

Comments
 (0)