Skip to content

Commit 839622d

Browse files
authored
Improve Event Grid Local Development Experience (#3984)
* first working poc * prompt for fields enclosed by {} * add button and code lens + clean up and refactor * fix rebase * more refactor and cleanup * fix change in tree * add license comments * import type * restructure * switch to using a wizard * keep the file open and remember the function associated with each file * add aka.ms link & add code lens at the bottom * switch to using AzExtFsExtra * wrap in try/finally * rename to save and execute * revert aka.ms link for now * fix codelens last line * remove commented out code * add source of event sources * revert styling changes in unrelated file * remove last line codelens * add licenses * PR nits * show info box only once per session * update to use workspaceState * PR feedback * remove weird defaulting * telemetry: entry point * telemetry: event source & type * telemetry: whether file was modified * check * use constants
1 parent 0ce8212 commit 839622d

14 files changed

+575
-56
lines changed

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,12 @@
350350
"command": "azureFunctions.viewProperties",
351351
"title": "%azureFunctions.viewProperties%",
352352
"category": "Azure Functions"
353+
},
354+
{
355+
"command": "azureFunctions.eventGrid.sendMockRequest",
356+
"title": "%azureFunctions.eventGrid.sendMockRequest%",
357+
"category": "Azure Functions",
358+
"icon": "$(notebook-execute)"
353359
}
354360
],
355361
"submenus": [
@@ -687,6 +693,14 @@
687693
"when": "resourceFilename==function.json",
688694
"group": "zzz_binding@1"
689695
}
696+
],
697+
"editor/title": [
698+
{
699+
"command": "azureFunctions.eventGrid.sendMockRequest",
700+
"arguments": ["${file}"],
701+
"when": "resourceFilename=~/.*.eventgrid.json$/",
702+
"group": "navigation@1"
703+
}
690704
]
691705
},
692706
"jsonValidation": [

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"azureFunctions.viewCommitInGitHub": "View Commit in GitHub",
106106
"azureFunctions.viewDeploymentLogs": "View Deployment Logs",
107107
"azureFunctions.viewProperties": "View Properties",
108+
"azureFunctions.eventGrid.sendMockRequest": "Save and execute...",
108109
"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)",
109110
"azureFunctions.walkthrough.functionsStart.create.title": "Create a new Azure Functions project",
110111
"azureFunctions.walkthrough.functionsStart.description": "Learn about Azure Functions and the Azure Functions extension for Visual Studio Code",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { CodeLens, Range, type CodeLensProvider } from 'vscode';
6+
import { EventGridExecuteFunctionEntryPoint } from '../../../constants';
7+
import { localize } from '../../../localize';
8+
9+
export class EventGridCodeLensProvider implements CodeLensProvider {
10+
public provideCodeLenses(): CodeLens[] {
11+
const firstLineLens = new CodeLens(new Range(0, 0, 0, 0));
12+
13+
firstLineLens.command = {
14+
title: localize('saveExecute', 'Save and execute'),
15+
command: 'azureFunctions.eventGrid.sendMockRequest',
16+
arguments: [EventGridExecuteFunctionEntryPoint.CodeLens]
17+
};
18+
19+
return [firstLineLens];
20+
}
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type IActionContext } from "@microsoft/vscode-azext-utils";
7+
import { type EventGridSource } from "./eventGridSources";
8+
9+
export interface EventGridExecuteFunctionContext extends IActionContext {
10+
eventSource?: EventGridSource;
11+
selectedFileName?: string;
12+
selectedFileUrl?: string;
13+
fileOpened?: boolean;
14+
}
15+
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { AzExtFsExtra, AzureWizardExecuteStep, callWithTelemetryAndErrorHandling, nonNullProp, type IActionContext } from "@microsoft/vscode-azext-utils";
7+
import * as os from 'os';
8+
import * as path from "path";
9+
import * as vscode from 'vscode';
10+
import { type Progress } from "vscode";
11+
import { ext } from "../../../extensionVariables";
12+
import { localize } from "../../../localize";
13+
import { feedUtils } from "../../../utils/feedUtils";
14+
import { type EventGridExecuteFunctionContext } from "./EventGridExecuteFunctionContext";
15+
16+
export class EventGridFileOpenStep extends AzureWizardExecuteStep<EventGridExecuteFunctionContext> {
17+
public priority: number;
18+
19+
public async execute(context: EventGridExecuteFunctionContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise<void> {
20+
const eventSource = nonNullProp(context, 'eventSource');
21+
const selectedFileName = nonNullProp(context, 'selectedFileName');
22+
const selectedFileUrl = nonNullProp(context, 'selectedFileUrl');
23+
24+
// Get selected contents of sample request
25+
const downloadingMsg: string = localize('downloadingSample', 'Downloading sample request...');
26+
progress.report({ message: downloadingMsg });
27+
const selectedFileContent = await feedUtils.getJsonFeed(context, selectedFileUrl);
28+
29+
// Create a temp file with the sample request & open in new window
30+
const openingFileMsg: string = localize('openingFile', 'Opening file...');
31+
progress.report({ message: openingFileMsg });
32+
const tempFilePath: string = await createTempSampleFile(eventSource, selectedFileName, selectedFileContent);
33+
const document: vscode.TextDocument = await vscode.workspace.openTextDocument(tempFilePath);
34+
await vscode.window.showTextDocument(document, {
35+
preview: false,
36+
});
37+
ext.fileToFunctionNodeMap.set(document.fileName, nonNullProp(ext, 'currentExecutingFunctionNode'));
38+
context.fileOpened = true;
39+
40+
// Request will be sent when the user clicks on the button or on the codelens link
41+
// Show the message only once per workspace
42+
if (!ext.context.workspaceState.get('didShowEventGridFileOpenMsg')) {
43+
const doneMsg = localize('modifyFile', "You can modify the file and then click the 'Save and execute' button to send the request.");
44+
void vscode.window.showInformationMessage(doneMsg);
45+
await ext.context.workspaceState.update('didShowEventGridFileOpenMsg', true);
46+
}
47+
48+
// Set a listener to track whether the file was modified before the request is sent
49+
let modifiedListenerDisposable: vscode.Disposable;
50+
void new Promise<void>((resolve, reject) => {
51+
modifiedListenerDisposable = vscode.workspace.onDidChangeTextDocument(async (event) => {
52+
if (event.contentChanges.length > 0 && event.document.fileName === document.fileName) {
53+
try {
54+
await callWithTelemetryAndErrorHandling('eventGridSampleModified', async (actionContext: IActionContext) => {
55+
actionContext.telemetry.properties.eventGridSampleModified = 'true';
56+
});
57+
resolve();
58+
} catch (error) {
59+
context.errorHandling.suppressDisplay = true;
60+
reject(error);
61+
} finally {
62+
modifiedListenerDisposable.dispose();
63+
}
64+
}
65+
});
66+
});
67+
68+
// Set a listener to delete the temp file after it's closed
69+
void new Promise<void>((resolve, reject) => {
70+
const closedListenerDisposable = vscode.workspace.onDidCloseTextDocument(async (closedDocument) => {
71+
if (closedDocument.fileName === document.fileName) {
72+
try {
73+
ext.fileToFunctionNodeMap.delete(document.fileName);
74+
await AzExtFsExtra.deleteResource(tempFilePath);
75+
resolve();
76+
} catch (error) {
77+
context.errorHandling.suppressDisplay = true;
78+
reject(error);
79+
} finally {
80+
closedListenerDisposable.dispose();
81+
if (modifiedListenerDisposable) {
82+
modifiedListenerDisposable.dispose();
83+
}
84+
}
85+
}
86+
});
87+
});
88+
89+
}
90+
91+
public shouldExecute(context: EventGridExecuteFunctionContext): boolean {
92+
return !context.fileOpened
93+
}
94+
95+
}
96+
97+
async function createTempSampleFile(eventSource: string, fileName: string, contents: {}): Promise<string> {
98+
const samplesDirPath = await getSamplesDirPath(eventSource);
99+
const sampleFileName = fileName.replace(/\.json$/, '.eventgrid.json');
100+
const filePath: string = path.join(samplesDirPath, sampleFileName);
101+
102+
await AzExtFsExtra.writeJSON(filePath, contents);
103+
104+
return filePath;
105+
}
106+
107+
async function getSamplesDirPath(eventSource: string): Promise<string> {
108+
// Create the path to the directory
109+
const baseDir: string = path.join(os.tmpdir(), 'vscode', 'azureFunctions', 'eventGridSamples');
110+
const dirPath = path.join(baseDir, eventSource);
111+
112+
// Create the directory if it doesn't already exist
113+
await AzExtFsExtra.ensureDir(dirPath);
114+
115+
// Return the path to the directory
116+
return dirPath;
117+
}
118+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { AzureWizardPromptStep, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils";
7+
import { localize } from "../../../localize";
8+
import { type EventGridExecuteFunctionContext } from "./EventGridExecuteFunctionContext";
9+
import { supportedEventGridSourceLabels, supportedEventGridSources, type EventGridSource } from "./eventGridSources";
10+
11+
export class EventGridSourceStep extends AzureWizardPromptStep<EventGridExecuteFunctionContext> {
12+
public hideStepCount: boolean = false;
13+
14+
public async prompt(context: EventGridExecuteFunctionContext): Promise<void> {
15+
// Prompt for event source
16+
const eventGridSourcePicks: IAzureQuickPickItem<EventGridSource | undefined>[] = supportedEventGridSources.map((source: EventGridSource) => {
17+
return {
18+
label: supportedEventGridSourceLabels.get(source) || source,
19+
data: source,
20+
};
21+
});
22+
const eventSource =
23+
(
24+
await context.ui.showQuickPick(eventGridSourcePicks, {
25+
placeHolder: localize('selectEventSource', 'Select the event source'),
26+
stepName: 'eventGridSource',
27+
})
28+
).data;
29+
30+
context.telemetry.properties.eventGridSource = eventSource;
31+
context.eventSource = eventSource;
32+
}
33+
34+
public shouldPrompt(context: EventGridExecuteFunctionContext): boolean {
35+
return !context.eventSource;
36+
}
37+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { AzureWizardPromptStep, nonNullProp, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils";
7+
import { localize } from "../../../localize";
8+
import { feedUtils } from "../../../utils/feedUtils";
9+
import { type EventGridExecuteFunctionContext } from "./EventGridExecuteFunctionContext";
10+
11+
const sampleFilesUrl =
12+
'https://api.github.com/repos/Azure/azure-rest-api-specs/contents/specification/eventgrid/data-plane/' +
13+
'{eventSource}' +
14+
'/stable/2018-01-01/examples/cloud-events-schema/';
15+
16+
type FileMetadata = {
17+
name: string;
18+
path: string;
19+
sha: string;
20+
size: number;
21+
url: string;
22+
html_url: string;
23+
git_url: string;
24+
download_url: string;
25+
type: string;
26+
_links: {
27+
self: string;
28+
git: string;
29+
html: string;
30+
};
31+
};
32+
33+
export class EventGridTypeStep extends AzureWizardPromptStep<EventGridExecuteFunctionContext> {
34+
public hideStepCount: boolean = false;
35+
36+
public async prompt(context: EventGridExecuteFunctionContext): Promise<void> {
37+
const eventSource = nonNullProp(context, 'eventSource');
38+
39+
// Get sample files for event source
40+
const samplesUrl = sampleFilesUrl.replace('{eventSource}', eventSource);
41+
const sampleFiles: FileMetadata[] = await feedUtils.getJsonFeed(context, samplesUrl);
42+
const fileNames: string[] = sampleFiles.map((fileMetadata) => fileMetadata.name);
43+
44+
// Prompt for event type
45+
const eventTypePicks: IAzureQuickPickItem<string | undefined>[] = fileNames.map((name: string) => ({
46+
data: name,
47+
// give human-readable name for event type from file name
48+
label: name
49+
.replace(/\.json$/, '')
50+
.split('_')
51+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
52+
.join(' '),
53+
}));
54+
55+
const selectedFileName =
56+
(
57+
await context.ui.showQuickPick(eventTypePicks, {
58+
placeHolder: localize('selectEventType', 'Select the event type'),
59+
stepName: 'eventType',
60+
})
61+
).data;
62+
63+
context.telemetry.properties.eventGridSample = selectedFileName;
64+
context.selectedFileName = selectedFileName;
65+
66+
context.selectedFileUrl = sampleFiles.find((fileMetadata) => fileMetadata.name === context.selectedFileName)?.download_url || sampleFiles[0].download_url;
67+
68+
}
69+
70+
public shouldPrompt(context: EventGridExecuteFunctionContext): boolean {
71+
return !context.selectedFileName;
72+
}
73+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/**
7+
* Sources here were obtained as the names of sources found on
8+
* the EventGrid samples in the azure-rest-api-specs repository:
9+
* https://github.com/Azure/azure-rest-api-specs/tree/master/specification/eventgrid/data-plane
10+
*/
11+
12+
export type EventGridSource =
13+
| 'Microsoft.ApiManagement'
14+
| 'Microsoft.AppConfiguration'
15+
| 'Microsoft.AVS'
16+
| 'Microsoft.Cache'
17+
| 'Microsoft.Communication'
18+
| 'Microsoft.ContainerRegistry'
19+
| 'Microsoft.ContainerService'
20+
| 'Microsoft.DataBox'
21+
| 'Microsoft.Devices'
22+
| 'Microsoft.EventHub'
23+
| 'Microsoft.HealthcareApis'
24+
| 'Microsoft.KeyVault'
25+
| 'Microsoft.MachineLearningServices'
26+
| 'Microsoft.Maps'
27+
| 'Microsoft.Media'
28+
| 'Microsoft.PolicyInsights'
29+
| 'Microsoft.ResourceNotification'
30+
| 'Microsoft.Resources'
31+
| 'Microsoft.ServiceBus'
32+
| 'Microsoft.SignalRService'
33+
| 'Microsoft.Storage'
34+
| 'Microsoft.Web'
35+
| string;
36+
37+
export const supportedEventGridSources: EventGridSource[] = [
38+
'Microsoft.ApiManagement',
39+
'Microsoft.AppConfiguration',
40+
'Microsoft.AVS',
41+
'Microsoft.Cache',
42+
'Microsoft.Communication',
43+
'Microsoft.ContainerRegistry',
44+
'Microsoft.ContainerService',
45+
'Microsoft.DataBox',
46+
'Microsoft.Devices',
47+
'Microsoft.EventHub',
48+
'Microsoft.HealthcareApis',
49+
'Microsoft.KeyVault',
50+
'Microsoft.MachineLearningServices',
51+
'Microsoft.Maps',
52+
'Microsoft.Media',
53+
'Microsoft.PolicyInsights',
54+
'Microsoft.ResourceNotification',
55+
'Microsoft.Resources',
56+
'Microsoft.ServiceBus',
57+
'Microsoft.SignalRService',
58+
'Microsoft.Storage',
59+
'Microsoft.Web',
60+
];
61+
62+
export const supportedEventGridSourceLabels: Map<EventGridSource, string> = new Map([
63+
['Microsoft.Storage', 'Blob Storage'],
64+
['Microsoft.EventHub', 'Event Hubs'],
65+
['Microsoft.ServiceBus', 'Service Bus'],
66+
['Microsoft.ContainerRegistry', 'Container Registry'],
67+
['Microsoft.ApiManagement', 'API Management'],
68+
['Microsoft.Resources', 'Resources'],
69+
['Microsoft.HealthcareApis', 'Health Data Services'],
70+
]);

0 commit comments

Comments
 (0)