diff --git a/package.json b/package.json index 1086fc5ca..d39b70daa 100644 --- a/package.json +++ b/package.json @@ -350,6 +350,12 @@ "command": "azureFunctions.viewProperties", "title": "%azureFunctions.viewProperties%", "category": "Azure Functions" + }, + { + "command": "azureFunctions.eventGrid.sendMockRequest", + "title": "%azureFunctions.eventGrid.sendMockRequest%", + "category": "Azure Functions", + "icon": "$(notebook-execute)" } ], "submenus": [ @@ -687,6 +693,14 @@ "when": "resourceFilename==function.json", "group": "zzz_binding@1" } + ], + "editor/title": [ + { + "command": "azureFunctions.eventGrid.sendMockRequest", + "arguments": ["${file}"], + "when": "resourceFilename=~/.*.eventgrid.json$/", + "group": "navigation@1" + } ] }, "jsonValidation": [ diff --git a/package.nls.json b/package.nls.json index 623517813..034bd110e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -105,6 +105,7 @@ "azureFunctions.viewCommitInGitHub": "View Commit in GitHub", "azureFunctions.viewDeploymentLogs": "View Deployment Logs", "azureFunctions.viewProperties": "View Properties", + "azureFunctions.eventGrid.sendMockRequest": "Save and execute...", "azureFunctions.walkthrough.functionsStart.create.description": "If you're just getting started, you will need to create an Azure Functions project. Follow along with the [Visual Studio Code developer guide](https://aka.ms/functions-getstarted-vscode) for step-by-step instructions.\n[Create New Project](command:azureFunctions.createNewProject)", "azureFunctions.walkthrough.functionsStart.create.title": "Create a new Azure Functions project", "azureFunctions.walkthrough.functionsStart.description": "Learn about Azure Functions and the Azure Functions extension for Visual Studio Code", diff --git a/src/commands/executeFunction/eventGrid/EventGridCodeLensProvider.ts b/src/commands/executeFunction/eventGrid/EventGridCodeLensProvider.ts new file mode 100644 index 000000000..1b2214fd4 --- /dev/null +++ b/src/commands/executeFunction/eventGrid/EventGridCodeLensProvider.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CodeLens, Range, type CodeLensProvider } from 'vscode'; +import { EventGridExecuteFunctionEntryPoint } from '../../../constants'; +import { localize } from '../../../localize'; + +export class EventGridCodeLensProvider implements CodeLensProvider { + public provideCodeLenses(): CodeLens[] { + const firstLineLens = new CodeLens(new Range(0, 0, 0, 0)); + + firstLineLens.command = { + title: localize('saveExecute', 'Save and execute'), + command: 'azureFunctions.eventGrid.sendMockRequest', + arguments: [EventGridExecuteFunctionEntryPoint.CodeLens] + }; + + return [firstLineLens]; + } +} diff --git a/src/commands/executeFunction/eventGrid/EventGridExecuteFunctionContext.ts b/src/commands/executeFunction/eventGrid/EventGridExecuteFunctionContext.ts new file mode 100644 index 000000000..5a7fca5a9 --- /dev/null +++ b/src/commands/executeFunction/eventGrid/EventGridExecuteFunctionContext.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { type EventGridSource } from "./eventGridSources"; + +export interface EventGridExecuteFunctionContext extends IActionContext { + eventSource?: EventGridSource; + selectedFileName?: string; + selectedFileUrl?: string; + fileOpened?: boolean; +} + diff --git a/src/commands/executeFunction/eventGrid/EventGridFileOpenStep.ts b/src/commands/executeFunction/eventGrid/EventGridFileOpenStep.ts new file mode 100644 index 000000000..0d5a5f473 --- /dev/null +++ b/src/commands/executeFunction/eventGrid/EventGridFileOpenStep.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzExtFsExtra, AzureWizardExecuteStep, callWithTelemetryAndErrorHandling, nonNullProp, type IActionContext } from "@microsoft/vscode-azext-utils"; +import * as os from 'os'; +import * as path from "path"; +import * as vscode from 'vscode'; +import { type Progress } from "vscode"; +import { ext } from "../../../extensionVariables"; +import { localize } from "../../../localize"; +import { feedUtils } from "../../../utils/feedUtils"; +import { type EventGridExecuteFunctionContext } from "./EventGridExecuteFunctionContext"; + +export class EventGridFileOpenStep extends AzureWizardExecuteStep { + public priority: number; + + public async execute(context: EventGridExecuteFunctionContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise { + const eventSource = nonNullProp(context, 'eventSource'); + const selectedFileName = nonNullProp(context, 'selectedFileName'); + const selectedFileUrl = nonNullProp(context, 'selectedFileUrl'); + + // Get selected contents of sample request + const downloadingMsg: string = localize('downloadingSample', 'Downloading sample request...'); + progress.report({ message: downloadingMsg }); + const selectedFileContent = await feedUtils.getJsonFeed(context, selectedFileUrl); + + // Create a temp file with the sample request & open in new window + const openingFileMsg: string = localize('openingFile', 'Opening file...'); + progress.report({ message: openingFileMsg }); + const tempFilePath: string = await createTempSampleFile(eventSource, selectedFileName, selectedFileContent); + const document: vscode.TextDocument = await vscode.workspace.openTextDocument(tempFilePath); + await vscode.window.showTextDocument(document, { + preview: false, + }); + ext.fileToFunctionNodeMap.set(document.fileName, nonNullProp(ext, 'currentExecutingFunctionNode')); + context.fileOpened = true; + + // Request will be sent when the user clicks on the button or on the codelens link + // Show the message only once per workspace + if (!ext.context.workspaceState.get('didShowEventGridFileOpenMsg')) { + const doneMsg = localize('modifyFile', "You can modify the file and then click the 'Save and execute' button to send the request."); + void vscode.window.showInformationMessage(doneMsg); + await ext.context.workspaceState.update('didShowEventGridFileOpenMsg', true); + } + + // Set a listener to track whether the file was modified before the request is sent + let modifiedListenerDisposable: vscode.Disposable; + void new Promise((resolve, reject) => { + modifiedListenerDisposable = vscode.workspace.onDidChangeTextDocument(async (event) => { + if (event.contentChanges.length > 0 && event.document.fileName === document.fileName) { + try { + await callWithTelemetryAndErrorHandling('eventGridSampleModified', async (actionContext: IActionContext) => { + actionContext.telemetry.properties.eventGridSampleModified = 'true'; + }); + resolve(); + } catch (error) { + context.errorHandling.suppressDisplay = true; + reject(error); + } finally { + modifiedListenerDisposable.dispose(); + } + } + }); + }); + + // Set a listener to delete the temp file after it's closed + void new Promise((resolve, reject) => { + const closedListenerDisposable = vscode.workspace.onDidCloseTextDocument(async (closedDocument) => { + if (closedDocument.fileName === document.fileName) { + try { + ext.fileToFunctionNodeMap.delete(document.fileName); + await AzExtFsExtra.deleteResource(tempFilePath); + resolve(); + } catch (error) { + context.errorHandling.suppressDisplay = true; + reject(error); + } finally { + closedListenerDisposable.dispose(); + if (modifiedListenerDisposable) { + modifiedListenerDisposable.dispose(); + } + } + } + }); + }); + + } + + public shouldExecute(context: EventGridExecuteFunctionContext): boolean { + return !context.fileOpened + } + +} + +async function createTempSampleFile(eventSource: string, fileName: string, contents: {}): Promise { + const samplesDirPath = await getSamplesDirPath(eventSource); + const sampleFileName = fileName.replace(/\.json$/, '.eventgrid.json'); + const filePath: string = path.join(samplesDirPath, sampleFileName); + + await AzExtFsExtra.writeJSON(filePath, contents); + + return filePath; +} + +async function getSamplesDirPath(eventSource: string): Promise { + // Create the path to the directory + const baseDir: string = path.join(os.tmpdir(), 'vscode', 'azureFunctions', 'eventGridSamples'); + const dirPath = path.join(baseDir, eventSource); + + // Create the directory if it doesn't already exist + await AzExtFsExtra.ensureDir(dirPath); + + // Return the path to the directory + return dirPath; +} + diff --git a/src/commands/executeFunction/eventGrid/EventGridSourceStep.ts b/src/commands/executeFunction/eventGrid/EventGridSourceStep.ts new file mode 100644 index 000000000..141d8db53 --- /dev/null +++ b/src/commands/executeFunction/eventGrid/EventGridSourceStep.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import { localize } from "../../../localize"; +import { type EventGridExecuteFunctionContext } from "./EventGridExecuteFunctionContext"; +import { supportedEventGridSourceLabels, supportedEventGridSources, type EventGridSource } from "./eventGridSources"; + +export class EventGridSourceStep extends AzureWizardPromptStep { + public hideStepCount: boolean = false; + + public async prompt(context: EventGridExecuteFunctionContext): Promise { + // Prompt for event source + const eventGridSourcePicks: IAzureQuickPickItem[] = supportedEventGridSources.map((source: EventGridSource) => { + return { + label: supportedEventGridSourceLabels.get(source) || source, + data: source, + }; + }); + const eventSource = + ( + await context.ui.showQuickPick(eventGridSourcePicks, { + placeHolder: localize('selectEventSource', 'Select the event source'), + stepName: 'eventGridSource', + }) + ).data; + + context.telemetry.properties.eventGridSource = eventSource; + context.eventSource = eventSource; + } + + public shouldPrompt(context: EventGridExecuteFunctionContext): boolean { + return !context.eventSource; + } +} diff --git a/src/commands/executeFunction/eventGrid/EventGridTypeStep.ts b/src/commands/executeFunction/eventGrid/EventGridTypeStep.ts new file mode 100644 index 000000000..5a7b873fd --- /dev/null +++ b/src/commands/executeFunction/eventGrid/EventGridTypeStep.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, nonNullProp, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import { localize } from "../../../localize"; +import { feedUtils } from "../../../utils/feedUtils"; +import { type EventGridExecuteFunctionContext } from "./EventGridExecuteFunctionContext"; + +const sampleFilesUrl = + 'https://api.github.com/repos/Azure/azure-rest-api-specs/contents/specification/eventgrid/data-plane/' + + '{eventSource}' + + '/stable/2018-01-01/examples/cloud-events-schema/'; + +type FileMetadata = { + name: string; + path: string; + sha: string; + size: number; + url: string; + html_url: string; + git_url: string; + download_url: string; + type: string; + _links: { + self: string; + git: string; + html: string; + }; +}; + +export class EventGridTypeStep extends AzureWizardPromptStep { + public hideStepCount: boolean = false; + + public async prompt(context: EventGridExecuteFunctionContext): Promise { + const eventSource = nonNullProp(context, 'eventSource'); + + // Get sample files for event source + const samplesUrl = sampleFilesUrl.replace('{eventSource}', eventSource); + const sampleFiles: FileMetadata[] = await feedUtils.getJsonFeed(context, samplesUrl); + const fileNames: string[] = sampleFiles.map((fileMetadata) => fileMetadata.name); + + // Prompt for event type + const eventTypePicks: IAzureQuickPickItem[] = fileNames.map((name: string) => ({ + data: name, + // give human-readable name for event type from file name + label: name + .replace(/\.json$/, '') + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '), + })); + + const selectedFileName = + ( + await context.ui.showQuickPick(eventTypePicks, { + placeHolder: localize('selectEventType', 'Select the event type'), + stepName: 'eventType', + }) + ).data; + + context.telemetry.properties.eventGridSample = selectedFileName; + context.selectedFileName = selectedFileName; + + context.selectedFileUrl = sampleFiles.find((fileMetadata) => fileMetadata.name === context.selectedFileName)?.download_url || sampleFiles[0].download_url; + + } + + public shouldPrompt(context: EventGridExecuteFunctionContext): boolean { + return !context.selectedFileName; + } +} diff --git a/src/commands/executeFunction/eventGrid/eventGridSources.ts b/src/commands/executeFunction/eventGrid/eventGridSources.ts new file mode 100644 index 000000000..240272ea7 --- /dev/null +++ b/src/commands/executeFunction/eventGrid/eventGridSources.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Sources here were obtained as the names of sources found on + * the EventGrid samples in the azure-rest-api-specs repository: + * https://github.com/Azure/azure-rest-api-specs/tree/master/specification/eventgrid/data-plane + */ + +export type EventGridSource = + | 'Microsoft.ApiManagement' + | 'Microsoft.AppConfiguration' + | 'Microsoft.AVS' + | 'Microsoft.Cache' + | 'Microsoft.Communication' + | 'Microsoft.ContainerRegistry' + | 'Microsoft.ContainerService' + | 'Microsoft.DataBox' + | 'Microsoft.Devices' + | 'Microsoft.EventHub' + | 'Microsoft.HealthcareApis' + | 'Microsoft.KeyVault' + | 'Microsoft.MachineLearningServices' + | 'Microsoft.Maps' + | 'Microsoft.Media' + | 'Microsoft.PolicyInsights' + | 'Microsoft.ResourceNotification' + | 'Microsoft.Resources' + | 'Microsoft.ServiceBus' + | 'Microsoft.SignalRService' + | 'Microsoft.Storage' + | 'Microsoft.Web' + | string; + +export const supportedEventGridSources: EventGridSource[] = [ + 'Microsoft.ApiManagement', + 'Microsoft.AppConfiguration', + 'Microsoft.AVS', + 'Microsoft.Cache', + 'Microsoft.Communication', + 'Microsoft.ContainerRegistry', + 'Microsoft.ContainerService', + 'Microsoft.DataBox', + 'Microsoft.Devices', + 'Microsoft.EventHub', + 'Microsoft.HealthcareApis', + 'Microsoft.KeyVault', + 'Microsoft.MachineLearningServices', + 'Microsoft.Maps', + 'Microsoft.Media', + 'Microsoft.PolicyInsights', + 'Microsoft.ResourceNotification', + 'Microsoft.Resources', + 'Microsoft.ServiceBus', + 'Microsoft.SignalRService', + 'Microsoft.Storage', + 'Microsoft.Web', +]; + +export const supportedEventGridSourceLabels: Map = new Map([ + ['Microsoft.Storage', 'Blob Storage'], + ['Microsoft.EventHub', 'Event Hubs'], + ['Microsoft.ServiceBus', 'Service Bus'], + ['Microsoft.ContainerRegistry', 'Container Registry'], + ['Microsoft.ApiManagement', 'API Management'], + ['Microsoft.Resources', 'Resources'], + ['Microsoft.HealthcareApis', 'Health Data Services'], +]); diff --git a/src/commands/executeFunction/eventGrid/executeEventGridFunction.ts b/src/commands/executeFunction/eventGrid/executeEventGridFunction.ts new file mode 100644 index 000000000..99b5816cd --- /dev/null +++ b/src/commands/executeFunction/eventGrid/executeEventGridFunction.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, type AzureWizardExecuteStep, type AzureWizardPromptStep, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { localize } from '../../../localize'; +import { type FunctionTreeItemBase } from '../../../tree/FunctionTreeItemBase'; +import { type IFunction } from '../../../workspace/LocalFunction'; +import { type EventGridExecuteFunctionContext } from './EventGridExecuteFunctionContext'; +import { EventGridFileOpenStep } from './EventGridFileOpenStep'; +import { EventGridSourceStep } from './EventGridSourceStep'; +import { EventGridTypeStep } from './EventGridTypeStep'; + +export async function executeEventGridFunction(context: IActionContext, _node: FunctionTreeItemBase | IFunction): Promise { + const title: string = localize('executeEGFunction', 'Execute Event Grid Function'); + + const promptSteps: AzureWizardPromptStep[] = [ + new EventGridSourceStep(), + new EventGridTypeStep(), + ]; + + const executeSteps: AzureWizardExecuteStep[] = [ + new EventGridFileOpenStep(), + ]; + + const wizardContext: EventGridExecuteFunctionContext = { + ...context, + eventSource: undefined, + selectedFileName: undefined, + selectedFileUrl: undefined, + fileOpened: false, + }; + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title, + promptSteps, + executeSteps, + showLoadingPrompt: true, + }); + + await wizard.prompt(); + await wizard.execute(); +} diff --git a/src/commands/executeFunction/eventGrid/sendEventGridRequest.ts b/src/commands/executeFunction/eventGrid/sendEventGridRequest.ts new file mode 100644 index 000000000..546969dce --- /dev/null +++ b/src/commands/executeFunction/eventGrid/sendEventGridRequest.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 { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { EventGridExecuteFunctionEntryPoint } from '../../../constants'; +import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; +import { executeFunctionWithInput } from '../executeFunction'; + +export async function sendEventGridRequest(context: IActionContext, entryPoint: string) { + context.telemetry.properties.eventGridExecuteEntryPoint = + entryPoint === EventGridExecuteFunctionEntryPoint.CodeLens + ? EventGridExecuteFunctionEntryPoint.CodeLens + : EventGridExecuteFunctionEntryPoint.TitleBarButton; + + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + const errorMsg = localize('noActiveTextEditor', 'No active text editor found.'); + throw new Error(errorMsg); + } + const document = activeEditor.document; + await document.save(); + const requestContent: string = document.getText(); + + const node = ext.fileToFunctionNodeMap.get(document.fileName); + + if (!node) { + const errorMsg = localize( + 'noFunctionBeingExecuted', + 'No function is currently being executed. ' + + 'This command is intended to be run while an EventGrid function is being executed. ' + + 'Please make sure to execute your EventGrid function.', + ); + throw new Error(errorMsg); + } + + await executeFunctionWithInput(context, requestContent, node); +} diff --git a/src/commands/executeFunction.ts b/src/commands/executeFunction/executeFunction.ts similarity index 53% rename from src/commands/executeFunction.ts rename to src/commands/executeFunction/executeFunction.ts index 2784951c5..f4d86a7e9 100644 --- a/src/commands/executeFunction.ts +++ b/src/commands/executeFunction/executeFunction.ts @@ -8,54 +8,79 @@ import { type SiteClient } from '@microsoft/vscode-azext-azureappservice'; import { parseError, type IActionContext } from '@microsoft/vscode-azext-utils'; import fetch from 'cross-fetch'; import { window } from 'vscode'; -import { type FuncVersion } from '../FuncVersion'; -import { functionFilter } from '../constants'; -import { ext } from '../extensionVariables'; -import { localize } from '../localize'; -import { FunctionTreeItemBase } from '../tree/FunctionTreeItemBase'; -import { type FuncHostRequest } from '../tree/IProjectTreeItem'; -import { RemoteFunctionTreeItem } from '../tree/remoteProject/RemoteFunctionTreeItem'; -import { nonNullValue } from '../utils/nonNull'; -import { requestUtils } from '../utils/requestUtils'; -import { type IFunction } from '../workspace/LocalFunction'; +import { type FuncVersion } from '../../FuncVersion'; +import { functionFilter } from '../../constants'; +import { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; +import { FunctionTreeItemBase } from '../../tree/FunctionTreeItemBase'; +import { type FuncHostRequest } from '../../tree/IProjectTreeItem'; +import { RemoteFunctionTreeItem } from '../../tree/remoteProject/RemoteFunctionTreeItem'; +import { nonNullValue } from '../../utils/nonNull'; +import { requestUtils } from '../../utils/requestUtils'; +import { type IFunction } from '../../workspace/LocalFunction'; +import { executeEventGridFunction } from './eventGrid/executeEventGridFunction'; export async function executeFunction(context: IActionContext, node?: FunctionTreeItemBase | IFunction): Promise { context.telemetry.eventVersion = 2; if (!node) { const noItemFoundErrorMessage: string = localize('noFunctions', 'No functions found.'); - node = await ext.rgApi.pickAppResource({ ...context, noItemFoundErrorMessage }, { - filter: functionFilter, - expectedChildContextValue: /Function;/i - }); + node = await ext.rgApi.pickAppResource( + { ...context, noItemFoundErrorMessage }, + { + filter: functionFilter, + expectedChildContextValue: /Function;/i, + }, + ); } - const func = node instanceof FunctionTreeItemBase ? node.function : node; + try { + ext.isExecutingFunction = true; + ext.currentExecutingFunctionNode = node; + + const func = node instanceof FunctionTreeItemBase ? node.function : node; + + const triggerBindingType: string | undefined = node.triggerBindingType; + context.telemetry.properties.triggerBindingType = triggerBindingType; - const triggerBindingType: string | undefined = node.triggerBindingType; - context.telemetry.properties.triggerBindingType = triggerBindingType; - - let functionInput: string | {} = ''; - if (!func.isTimerTrigger) { - const prompt: string = localize('enterRequestBody', 'Enter request body'); - let value: string | undefined; - if (triggerBindingType) { - const version: FuncVersion = await node.project.getVersion(context); - const templateProvider = ext.templateProvider.get(context); - value = await templateProvider.tryGetSampleData(context, version, triggerBindingType); - if (value) { - // Clean up the whitespace to make it more friendly for a one-line input box - value = value.replace(/[\r\n\t]/g, ' '); - value = value.replace(/ +/g, ' '); + let functionInput: string | {} = ''; + if (triggerBindingType === 'eventGridTrigger') { + return await executeEventGridFunction(context, node); + } else if (!func.isTimerTrigger) { + const prompt: string = localize('enterRequestBody', 'Enter request body'); + let value: string | undefined; + if (triggerBindingType) { + const version: FuncVersion = await node.project.getVersion(context); + const templateProvider = ext.templateProvider.get(context); + value = await templateProvider.tryGetSampleData(context, version, triggerBindingType); + if (value) { + // Clean up the whitespace to make it more friendly for a one-line input box + value = value.replace(/[\r\n\t]/g, ' '); + value = value.replace(/ +/g, ' '); + } } - } - const data: string = await context.ui.showInputBox({ prompt, value, stepName: 'requestBody' }); - try { - functionInput = <{}>JSON.parse(data); - } catch { - functionInput = data; + const data: string = await context.ui.showInputBox({ + prompt, + value, + stepName: 'requestBody', + }); + try { + functionInput = <{}>JSON.parse(data); + } catch { + functionInput = data; + } } + + await executeFunctionWithInput(context, functionInput, node); + + } finally { + ext.isExecutingFunction = false; + ext.currentExecutingFunctionNode = undefined; } +} + +export async function executeFunctionWithInput(context: IActionContext, functionInput: string | {}, node: FunctionTreeItemBase | IFunction) { + const func = node instanceof FunctionTreeItemBase ? node.function : node; let triggerRequest: FuncHostRequest; let body: {}; @@ -80,7 +105,14 @@ export async function executeFunction(context: IActionContext, node?: FunctionTr headers.set('x-functions-key', (await client.listHostKeys()).masterKey ?? ''); } try { - responseText = (await requestUtils.sendRequestWithExtTimeout(context, { method: 'POST', ...triggerRequest, headers, body: JSON.stringify(body) })).bodyAsText; + responseText = ( + await requestUtils.sendRequestWithExtTimeout(context, { + method: 'POST', + ...triggerRequest, + headers, + body: JSON.stringify(body), + }) + ).bodyAsText; } catch (error) { const errorType = parseError(error).errorType; if (!client && errorType === 'ECONNREFUSED') { @@ -101,7 +133,7 @@ export async function executeFunction(context: IActionContext, node?: FunctionTr throw error; } } - } + }; if (node instanceof FunctionTreeItemBase) { await node.runWithTemporaryDescription(context, localize('executing', 'Executing...'), async () => { @@ -113,4 +145,7 @@ export async function executeFunction(context: IActionContext, node?: FunctionTr const message: string = responseText ? localize('executedWithResponse', 'Executed function "{0}". Response: "{1}"', func.name, responseText) : localize('executed', 'Executed function "{0}"', func.name); void window.showInformationMessage(message); + + ext.isExecutingFunction = false; + ext.currentExecutingFunctionNode = undefined; } diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 376a7800f..28fd13a9a 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -5,8 +5,15 @@ import { registerSiteCommand } from '@microsoft/vscode-azext-azureappservice'; import { AppSettingTreeItem, AppSettingsTreeItem } from '@microsoft/vscode-azext-azureappsettings'; -import { registerCommand, registerCommandWithTreeNodeUnwrapping, unwrapTreeNodeCommandCallback, type AzExtParentTreeItem, type AzExtTreeItem, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { commands } from "vscode"; +import { + registerCommand, + registerCommandWithTreeNodeUnwrapping, + unwrapTreeNodeCommandCallback, + type AzExtParentTreeItem, + type AzExtTreeItem, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; +import { commands, languages } from 'vscode'; import { getAgentBenchmarkConfigs, getCommands, runWizardCommandWithInputs, runWizardCommandWithoutExecution } from '../agent/agentIntegration'; import { ext } from '../extensionVariables'; import { installOrUpdateFuncCoreTools } from '../funcCoreTools/installOrUpdateFuncCoreTools'; @@ -38,7 +45,9 @@ import { redeployDeployment } from './deployments/redeployDeployment'; import { viewCommitInGitHub } from './deployments/viewCommitInGitHub'; import { viewDeploymentLogs } from './deployments/viewDeploymentLogs'; import { editAppSetting } from './editAppSetting'; -import { executeFunction } from './executeFunction'; +import { EventGridCodeLensProvider } from './executeFunction/eventGrid/EventGridCodeLensProvider'; +import { sendEventGridRequest } from './executeFunction/eventGrid/sendEventGridRequest'; +import { executeFunction } from './executeFunction/executeFunction'; import { initProjectForVSCode } from './initProjectForVSCode/initProjectForVSCode'; import { startStreamingLogs } from './logstream/startStreamingLogs'; import { stopStreamingLogs } from './logstream/stopStreamingLogs'; @@ -56,16 +65,22 @@ import { disableFunction, enableFunction } from './updateDisabledState'; import { viewProperties } from './viewProperties'; export function registerCommands(): void { - commands.registerCommand('azureFunctions.agent.getCommands', getCommands); commands.registerCommand('azureFunctions.agent.runWizardCommandWithoutExecution', runWizardCommandWithoutExecution); commands.registerCommand('azureFunctions.agent.runWizardCommandWithInputs', runWizardCommandWithInputs); commands.registerCommand('azureFunctions.agent.getAgentBenchmarkConfigs', getAgentBenchmarkConfigs); registerCommandWithTreeNodeUnwrapping('azureFunctions.addBinding', addBinding); - registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.add', async (context: IActionContext, node?: AzExtParentTreeItem) => await createChildNode(context, new RegExp(AppSettingsTreeItem.contextValue), node)); + registerCommandWithTreeNodeUnwrapping( + 'azureFunctions.appSettings.add', + async (context: IActionContext, node?: AzExtParentTreeItem) => + await createChildNode(context, new RegExp(AppSettingsTreeItem.contextValue), node), + ); registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.decrypt', decryptLocalSettings); - registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.delete', async (context: IActionContext, node?: AzExtTreeItem) => await deleteNode(context, new RegExp(AppSettingTreeItem.contextValue), node)); + registerCommandWithTreeNodeUnwrapping( + 'azureFunctions.appSettings.delete', + async (context: IActionContext, node?: AzExtTreeItem) => await deleteNode(context, new RegExp(AppSettingTreeItem.contextValue), node), + ); registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.download', downloadAppSettings); registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.edit', editAppSetting); registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.encrypt', encryptLocalSettings); @@ -80,11 +95,21 @@ 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.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); - registerCommandWithTreeNodeUnwrapping('azureFunctions.deleteSlot', async (context: IActionContext, node?: AzExtTreeItem) => await deleteNode(context, ResolvedFunctionAppResource.pickSlotContextValue, node)); + registerCommandWithTreeNodeUnwrapping( + 'azureFunctions.deleteSlot', + async (context: IActionContext, node?: AzExtTreeItem) => await deleteNode(context, ResolvedFunctionAppResource.pickSlotContextValue, node), + ); registerCommandWithTreeNodeUnwrapping('azureFunctions.disableFunction', disableFunction); registerCommandWithTreeNodeUnwrapping('azureFunctions.deploy', deployProductionSlot); registerCommandWithTreeNodeUnwrapping('azureFunctions.deploySlot', deploySlot); @@ -95,7 +120,9 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping('azureFunctions.installOrUpdateFuncCoreTools', installOrUpdateFuncCoreTools); registerCommandWithTreeNodeUnwrapping('azureFunctions.openFile', openFile); registerCommandWithTreeNodeUnwrapping('azureFunctions.openInPortal', openDeploymentInPortal); - registerCommand('azureFunctions.openWalkthrough', () => commands.executeCommand('workbench.action.openWalkthrough', 'ms-azuretools.vscode-azurefunctions#functionsStart')); + registerCommand('azureFunctions.openWalkthrough', () => + commands.executeCommand('workbench.action.openWalkthrough', 'ms-azuretools.vscode-azurefunctions#functionsStart'), + ); registerCommandWithTreeNodeUnwrapping('azureFunctions.pickProcess', pickFuncProcess); registerCommandWithTreeNodeUnwrapping('azureFunctions.redeploy', redeployDeployment); registerCommandWithTreeNodeUnwrapping('azureFunctions.restartFunctionApp', restartFunctionApp); @@ -107,10 +134,21 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping('azureFunctions.stopFunctionApp', stopFunctionApp); registerCommandWithTreeNodeUnwrapping('azureFunctions.stopStreamingLogs', stopStreamingLogs); registerCommandWithTreeNodeUnwrapping('azureFunctions.swapSlot', swapSlot); - registerCommandWithTreeNodeUnwrapping('azureFunctions.toggleAppSettingVisibility', async (context: IActionContext, node: AppSettingTreeItem) => { await node.toggleValueVisibility(context); }, 250); + registerCommandWithTreeNodeUnwrapping( + 'azureFunctions.toggleAppSettingVisibility', + async (context: IActionContext, node: AppSettingTreeItem) => { + await node.toggleValueVisibility(context); + }, + 250, + ); registerCommandWithTreeNodeUnwrapping('azureFunctions.uninstallFuncCoreTools', uninstallFuncCoreTools); registerCommandWithTreeNodeUnwrapping('azureFunctions.viewCommitInGitHub', viewCommitInGitHub); registerSiteCommand('azureFunctions.viewDeploymentLogs', unwrapTreeNodeCommandCallback(viewDeploymentLogs)); registerCommandWithTreeNodeUnwrapping('azureFunctions.viewProperties', viewProperties); - registerCommandWithTreeNodeUnwrapping('azureFunctions.showOutputChannel', () => { ext.outputChannel.show(); }); + registerCommandWithTreeNodeUnwrapping('azureFunctions.showOutputChannel', () => { + ext.outputChannel.show(); + }); + ext.eventGridProvider = new EventGridCodeLensProvider(); + ext.context.subscriptions.push(languages.registerCodeLensProvider({ pattern: '**/*.eventgrid.json' }, ext.eventGridProvider)); + registerCommand('azureFunctions.eventGrid.sendMockRequest', sendEventGridRequest); } diff --git a/src/constants.ts b/src/constants.ts index cd5cb8f35..e0adafeb8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -201,5 +201,10 @@ export enum ActionType { } export const noRuntimeStacksAvailableLabel = localize('noRuntimeStacksAvailable', 'No valid runtime stacks available'); +export enum EventGridExecuteFunctionEntryPoint { + CodeLens = 'CodeLens', + TitleBarButton = 'TitleBarButton' +} + // Originally from the Docker extension: https://github.com/microsoft/vscode-docker/blob/main/src/constants.ts export const dockerfileGlobPattern = '{*.[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE],[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE],[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE].*}'; diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 921574454..b52cdfc2d 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -3,12 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type IActionContext, type IAzExtOutputChannel, type IExperimentationServiceAdapter } from "@microsoft/vscode-azext-utils"; -import { type AzureHostExtensionApi } from "@microsoft/vscode-azext-utils/hostapi"; -import { type ExtensionContext } from "vscode"; -import { func } from "./constants"; -import { type CentralTemplateProvider } from "./templates/CentralTemplateProvider"; -import { type AzureAccountTreeItemWithProjects } from "./tree/AzureAccountTreeItemWithProjects"; +import { type IActionContext, type IAzExtOutputChannel, type IExperimentationServiceAdapter } from '@microsoft/vscode-azext-utils'; +import { type AzureHostExtensionApi } from '@microsoft/vscode-azext-utils/hostapi'; +import { type ExtensionContext } from 'vscode'; +import { type EventGridCodeLensProvider } from './commands/executeFunction/eventGrid/EventGridCodeLensProvider'; +import { func } from './constants'; +import { type CentralTemplateProvider } from './templates/CentralTemplateProvider'; +import { type AzureAccountTreeItemWithProjects } from './tree/AzureAccountTreeItemWithProjects'; +import { type FunctionTreeItemBase } from './tree/FunctionTreeItemBase'; +import { type IFunction } from './workspace/LocalFunction'; /** * Used for extensionVariables that can also be set per-action @@ -54,10 +57,14 @@ export namespace ext { export let experimentationService: IExperimentationServiceAdapter; export const templateProvider = new ActionVariable('_centralTemplateProvider'); export let rgApi: AzureHostExtensionApi; + export let eventGridProvider: EventGridCodeLensProvider; + export let currentExecutingFunctionNode: FunctionTreeItemBase | IFunction | undefined; + export const fileToFunctionNodeMap: Map = new Map(); + export let isExecutingFunction: boolean | undefined; } export enum TemplateSource { Backup = 'Backup', Latest = 'Latest', - Staging = 'Staging' + Staging = 'Staging', }