Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
efa7e82
first working poc
hossam-nasr Jan 19, 2024
f8f4558
prompt for fields enclosed by {}
hossam-nasr Jan 19, 2024
9995b87
add button and code lens + clean up and refactor
hossam-nasr Feb 20, 2024
6c48f01
fix rebase
hossam-nasr Feb 20, 2024
9fa9774
more refactor and cleanup
hossam-nasr Feb 20, 2024
8c646aa
fix change in tree
hossam-nasr Feb 20, 2024
7c77ae7
add license comments
hossam-nasr Feb 20, 2024
20cc9e6
import type
hossam-nasr Feb 20, 2024
3a1425d
restructure
hossam-nasr Feb 29, 2024
00089ec
switch to using a wizard
hossam-nasr Feb 29, 2024
1ea43c6
keep the file open and remember the function associated with each file
hossam-nasr Feb 29, 2024
2adbf39
add aka.ms link & add code lens at the bottom
hossam-nasr Feb 29, 2024
f043928
switch to using AzExtFsExtra
hossam-nasr Feb 29, 2024
e0a3cb0
wrap in try/finally
hossam-nasr Feb 29, 2024
012a593
rename to save and execute
hossam-nasr Feb 29, 2024
8ee991e
revert aka.ms link for now
hossam-nasr Feb 29, 2024
203c151
fix codelens last line
hossam-nasr Feb 29, 2024
9b2eeb7
remove commented out code
hossam-nasr Mar 5, 2024
acd4317
add source of event sources
hossam-nasr Mar 5, 2024
79ea4f3
revert styling changes in unrelated file
hossam-nasr Mar 5, 2024
b2029b4
remove last line codelens
hossam-nasr Mar 5, 2024
1ceca9e
add licenses
hossam-nasr Mar 5, 2024
4eb5e40
PR nits
hossam-nasr Mar 5, 2024
b17f5e8
show info box only once per session
hossam-nasr Mar 5, 2024
907ce44
update to use workspaceState
hossam-nasr Mar 5, 2024
3a93fe6
PR feedback
hossam-nasr Mar 9, 2024
3c055fd
remove weird defaulting
hossam-nasr Mar 9, 2024
bdf6a59
telemetry: entry point
hossam-nasr Mar 9, 2024
b24e300
telemetry: event source & type
hossam-nasr Mar 9, 2024
774e414
telemetry: whether file was modified
hossam-nasr Mar 9, 2024
4241096
check
hossam-nasr Mar 9, 2024
99333f8
use constants
hossam-nasr Mar 13, 2024
e90ead8
Merge branch 'main' into hossamnasr/eventgrid-lde
hossam-nasr Mar 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,12 @@
"command": "azureFunctions.viewProperties",
"title": "%azureFunctions.viewProperties%",
"category": "Azure Functions"
},
{
"command": "azureFunctions.eventGrid.sendMockRequest",
Comment thread
hossam-nasr marked this conversation as resolved.
"title": "%azureFunctions.eventGrid.sendMockRequest%",
"category": "Azure Functions",
"icon": "$(notebook-execute)"
}
],
"submenus": [
Expand Down Expand Up @@ -687,6 +693,13 @@
"when": "resourceFilename==function.json",
"group": "zzz_binding@1"
}
],
"editor/title": [
{
"command": "azureFunctions.eventGrid.sendMockRequest",
"when": "resourceFilename=~/.*.eventgrid.json$/",
Comment thread
hossam-nasr marked this conversation as resolved.
"group": "navigation@1"
}
]
},
"jsonValidation": [
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"azureFunctions.viewCommitInGitHub": "View Commit in GitHub",
"azureFunctions.viewDeploymentLogs": "View Deployment Logs",
"azureFunctions.viewProperties": "View Properties",
"azureFunctions.eventGrid.sendMockRequest": "Save and send request...",
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
"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",
Expand Down
14 changes: 14 additions & 0 deletions src/commands/executeFunction/EventGridCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CodeLens, CodeLensProvider, Range } from 'vscode';
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
import { localize } from '../../localize';

export class EventGridCodeLensProvider implements CodeLensProvider {
public provideCodeLenses(): CodeLens[] {
const sendRequestLens = new CodeLens(new Range(0, 0, 0, 0));
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
sendRequestLens.command = {
title: localize('saveSendRequest', 'Save and send request'),
command: 'azureFunctions.eventGrid.sendMockRequest',
};

return [sendRequestLens];
}
}
59 changes: 59 additions & 0 deletions src/commands/executeFunction/eventGridSources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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<EventGridSource, string> = 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'],
]);
156 changes: 156 additions & 0 deletions src/commands/executeFunction/executeEventGridFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import { localize } from '../../localize';
import { FunctionTreeItemBase } from '../../tree/FunctionTreeItemBase';
import { feedUtils } from '../../utils/feedUtils';
import { IFunction } from '../../workspace/LocalFunction';
import { EventGridSource, supportedEventGridSourceLabels, supportedEventGridSources } from './eventGridSources';
import { executeFunctionWithInput } from './executeFunction';

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;
};
};

const sampleFilesUrl =
'https://api.github.com/repos/Azure/azure-rest-api-specs/contents/specification/eventgrid/data-plane/' +
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
'{eventSource}' +
'/stable/2018-01-01/examples/cloud-events-schema/';

export async function executeEventGridFunction(context: IActionContext, node: FunctionTreeItemBase | IFunction): Promise<void> {
// Prompt for event source
const eventGridSourcePicks: IAzureQuickPickItem<EventGridSource | undefined>[] = supportedEventGridSources.map((source: EventGridSource) => {
return {
label: supportedEventGridSourceLabels.get(source) || source,
data: source,
};
});
const eventSource: EventGridSource =
(
await context.ui.showQuickPick(eventGridSourcePicks, {
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
placeHolder: localize('selectEventSource', 'Select the event source'),
stepName: 'eventGridSource',
})
).data ?? 'Microsoft.Storage';

// 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<string | undefined>[] = 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: string =
(
await context.ui.showQuickPick(eventTypePicks, {
placeHolder: localize('selectEventType', 'Select the event type'),
stepName: 'eventType',
})
).data ?? 'blob_created.json';

// Get selected contents of sample request
const selectedFileUrl = sampleFiles.find((fileMetadata) => fileMetadata.name === selectedFileName)?.download_url || sampleFiles[0].download_url;
const selectedFileContents: {} = await feedUtils.getJsonFeed(context, selectedFileUrl);

// Prompt for whether to send or modify the sample request
const shouldModify: boolean =
(
await context.ui.showQuickPick(
[
{
label: localize('sendSample', 'Send sample request'),
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
data: false,
},
{
label: localize('modifySample', 'Modify sample request'),
data: true,
},
],
{
placeHolder: 'Would you like to send the sample request or modify it first?',
stepName: 'modifyOrSendSample',
},
)
).data || false;

// Execute function with sample data directly if user chooses not to modify
if (!shouldModify) {
return executeFunctionWithInput(context, selectedFileContents, node);
}

// Create a temp file with the sample request & open in new window
const tempFilePath: string = await createTempSampleFile(eventSource, selectedFileName, selectedFileContents);
const document: vscode.TextDocument = await vscode.workspace.openTextDocument(tempFilePath);
await vscode.window.showTextDocument(document, {
preview: false,
});

// Request will be sent when the user clicks on the button or on the codelens link

// Set a listener to delete the temp file after it's closed
await new Promise<void>((resolve, reject) => {
const disposable = vscode.workspace.onDidCloseTextDocument(async (closedDocument) => {
if (closedDocument.fileName === document.fileName) {
try {
await fs.unlink(tempFilePath);
resolve();
} catch (error) {
reject(error);
} finally {
disposable.dispose();
}
}
});
});
}

async function createTempSampleFile(eventSource: string, fileName: string, contents: {}): Promise<string> {
const samplesDirPath = await createSamplesDirIfNotExists(eventSource);
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
const sampleFileName = fileName.replace(/\.json$/, '.eventgrid.json');
const filePath: string = path.join(samplesDirPath, sampleFileName);

await fs.writeFile(filePath, JSON.stringify(contents, undefined, 2));
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated

return filePath;
}

async function createSamplesDirIfNotExists(eventSource: string): Promise<string> {
const baseDir: string = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || path.join(os.tmpdir(), 'vscode', 'azureFunctions');

// Create the path to the directory
const dirPath = path.join(baseDir, '.vscode', 'eventGridSamples', eventSource);
// Create the directory if it doesn't already exist
if (!(await fs.pathExists(dirPath))) {
Comment thread
hossam-nasr marked this conversation as resolved.
Outdated
await fs.mkdirp(dirPath);
}
// Return the path to the directory
return dirPath;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,43 @@ 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 './executeEventGridFunction';

export async function executeFunction(context: IActionContext, node?: FunctionTreeItemBase | IFunction): Promise<void> {
context.telemetry.eventVersion = 2;
if (!node) {
const noItemFoundErrorMessage: string = localize('noFunctions', 'No functions found.');
node = await ext.rgApi.pickAppResource<FunctionTreeItemBase>({ ...context, noItemFoundErrorMessage }, {
filter: functionFilter,
expectedChildContextValue: /Function;/i
});
node = await ext.rgApi.pickAppResource<FunctionTreeItemBase>(
{ ...context, noItemFoundErrorMessage },
{
filter: functionFilter,
expectedChildContextValue: /Function;/i,
},
);
}

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;

let functionInput: string | {} = '';
if (!func.isTimerTrigger) {
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) {
Expand All @@ -49,14 +58,26 @@ export async function executeFunction(context: IActionContext, node?: FunctionTr
}
}

const data: string = await context.ui.showInputBox({ prompt, value, stepName: 'requestBody' });
const data: string = await context.ui.showInputBox({
prompt,
value,
stepName: 'requestBody',
});
try {
functionInput = <{}>JSON.parse(data);
} catch {
functionInput = data;
}
}

await executeFunctionWithInput(context, functionInput, node);
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: {};
if (func.isHttpTrigger) {
Expand All @@ -80,7 +101,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') {
Expand All @@ -101,7 +129,7 @@ export async function executeFunction(context: IActionContext, node?: FunctionTr
throw error;
}
}
}
};

if (node instanceof FunctionTreeItemBase) {
await node.runWithTemporaryDescription(context, localize('executing', 'Executing...'), async () => {
Expand All @@ -113,4 +141,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;
Comment thread
hossam-nasr marked this conversation as resolved.
ext.currentExecutingFunctionNode = undefined;
}
Loading