Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Event Grid Local Development Experience #3984

Merged
merged 33 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 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",
hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
"title": "%azureFunctions.eventGrid.sendMockRequest%",
"category": "Azure Functions",
"icon": "$(notebook-execute)"
}
],
"submenus": [
Expand Down Expand Up @@ -687,6 +693,14 @@
"when": "resourceFilename==function.json",
"group": "zzz_binding@1"
}
],
"editor/title": [
{
"command": "azureFunctions.eventGrid.sendMockRequest",
"arguments": ["${file}"],
hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
"when": "resourceFilename=~/.*.eventgrid.json$/",
hossam-nasr marked this conversation as resolved.
Show resolved Hide 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 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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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];
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

118 changes: 118 additions & 0 deletions src/commands/executeFunction/eventGrid/EventGridFileOpenStep.ts
Original file line number Diff line number Diff line change
@@ -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<EventGridExecuteFunctionContext> {
public priority: number;

public async execute(context: EventGridExecuteFunctionContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise<void> {
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);
}

hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
// Set a listener to track whether the file was modified before the request is sent
let modifiedListenerDisposable: vscode.Disposable;
void new Promise<void>((resolve, reject) => {
modifiedListenerDisposable = vscode.workspace.onDidChangeTextDocument(async (event) => {
nturinski marked this conversation as resolved.
Show resolved Hide resolved
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<void>((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);
hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
} finally {
closedListenerDisposable.dispose();
if (modifiedListenerDisposable) {
modifiedListenerDisposable.dispose();
}
}
}
});
});

}

public shouldExecute(context: EventGridExecuteFunctionContext): boolean {
return !context.fileOpened
}

}

async function createTempSampleFile(eventSource: string, fileName: string, contents: {}): Promise<string> {
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<string> {
// 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;
}

37 changes: 37 additions & 0 deletions src/commands/executeFunction/eventGrid/EventGridSourceStep.ts
Original file line number Diff line number Diff line change
@@ -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<EventGridExecuteFunctionContext> {
public hideStepCount: boolean = false;

public async prompt(context: EventGridExecuteFunctionContext): 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 =
(
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;
}
}
73 changes: 73 additions & 0 deletions src/commands/executeFunction/eventGrid/EventGridTypeStep.ts
Original file line number Diff line number Diff line change
@@ -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<EventGridExecuteFunctionContext> {
public hideStepCount: boolean = false;

public async prompt(context: EventGridExecuteFunctionContext): Promise<void> {
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<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 =
(
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;
}
}
70 changes: 70 additions & 0 deletions src/commands/executeFunction/eventGrid/eventGridSources.ts
Original file line number Diff line number Diff line change
@@ -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.
*--------------------------------------------------------------------------------------------*/

hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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<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'],
]);
Loading
Loading