Retrieve templates from functions portal instead of cli (#32)
The main benefits: 1. We get the full list of templates supported in the portal 1. We can prompt the user for input parameters 1. We reduce reliance on the func cli I also refactored uiUtil into an interface so that I could more easily create unit tests
This commit is contained in:
Родитель
2223cbf25a
Коммит
38d21f6148
|
@ -214,6 +214,8 @@
|
|||
"devDependencies": {
|
||||
"@types/mocha": "^2.2.32",
|
||||
"@types/node": "^8.0.28",
|
||||
"@types/request-promise": "^4.1.38",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"mocha": "^2.3.3",
|
||||
"tslint": "^5.7.0",
|
||||
"tslint-microsoft-contrib": "5.0.1",
|
||||
|
@ -221,11 +223,14 @@
|
|||
"vscode": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"azure-arm-cosmosdb": "^1.0.0-preview",
|
||||
"azure-arm-resource": "^2.0.0-preview",
|
||||
"azure-arm-website": "^1.0.0-preview",
|
||||
"ms-rest": "^2.2.2",
|
||||
"ms-rest-azure": "^2.3.1",
|
||||
"opn": "^5.1.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"vscode-extension-telemetry": "^0.0.6",
|
||||
"vscode-nls": "^2.0.2",
|
||||
"vscode-azureappservice": "^0.1.1"
|
||||
|
|
|
@ -5,22 +5,25 @@
|
|||
|
||||
import { Event, EventEmitter, TreeDataProvider, TreeItem } from 'vscode';
|
||||
import { AzureAccount, AzureResourceFilter } from './azure-account.api';
|
||||
import { IUserInterface, PickWithData } from './IUserInterface';
|
||||
import { localize } from './localize';
|
||||
import { NodeBase } from './nodes/NodeBase';
|
||||
import { SubscriptionNode } from './nodes/SubscriptionNode';
|
||||
import * as uiUtil from './utils/ui';
|
||||
import { VSCodeUI } from './VSCodeUI';
|
||||
|
||||
export class AzureFunctionsExplorer implements TreeDataProvider<NodeBase> {
|
||||
private onDidChangeTreeDataEmitter: EventEmitter<NodeBase> = new EventEmitter<NodeBase>();
|
||||
private azureAccount: AzureAccount;
|
||||
private rootNodes: NodeBase[] = [];
|
||||
private _onDidChangeTreeDataEmitter: EventEmitter<NodeBase> = new EventEmitter<NodeBase>();
|
||||
private _azureAccount: AzureAccount;
|
||||
private _rootNodes: NodeBase[] = [];
|
||||
private _ui: IUserInterface;
|
||||
|
||||
constructor(azureAccount: AzureAccount) {
|
||||
this.azureAccount = azureAccount;
|
||||
constructor(azureAccount: AzureAccount, ui: IUserInterface = new VSCodeUI()) {
|
||||
this._azureAccount = azureAccount;
|
||||
this._ui = ui;
|
||||
}
|
||||
|
||||
public get onDidChangeTreeData(): Event<NodeBase> {
|
||||
return this.onDidChangeTreeDataEmitter.event;
|
||||
return this._onDidChangeTreeDataEmitter.event;
|
||||
}
|
||||
|
||||
public getTreeItem(node: NodeBase): TreeItem {
|
||||
|
@ -31,40 +34,40 @@ export class AzureFunctionsExplorer implements TreeDataProvider<NodeBase> {
|
|||
if (node) {
|
||||
return await node.getChildren();
|
||||
} else { // Root of the explorer
|
||||
this.rootNodes = [];
|
||||
this._rootNodes = [];
|
||||
|
||||
if (this.azureAccount.status === 'Initializing' || this.azureAccount.status === 'LoggingIn') {
|
||||
if (this._azureAccount.status === 'Initializing' || this._azureAccount.status === 'LoggingIn') {
|
||||
return [new NodeBase('azureFunctionsLoading', localize('azFunc.loadingNode', 'Loading...'))];
|
||||
} else if (this.azureAccount.status === 'LoggedOut') {
|
||||
} else if (this._azureAccount.status === 'LoggedOut') {
|
||||
return [new NodeBase('azureFunctionsSignInToAzure', localize('azFunc.signInNode', 'Sign in to Azure...'), undefined, 'azure-account.login')];
|
||||
} else if (this.azureAccount.filters.length === 0) {
|
||||
} else if (this._azureAccount.filters.length === 0) {
|
||||
return [new NodeBase('azureFunctionsNoSubscriptions', localize('azFunc.noSubscriptionsNode', 'No subscriptions found. Edit filters...'), undefined, 'azure-account.selectSubscriptions')];
|
||||
} else {
|
||||
this.rootNodes = this.azureAccount.filters.map((filter: AzureResourceFilter) => SubscriptionNode.CREATE(filter));
|
||||
this._rootNodes = this._azureAccount.filters.map((filter: AzureResourceFilter) => SubscriptionNode.CREATE(filter));
|
||||
|
||||
return this.rootNodes;
|
||||
return this._rootNodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(node?: NodeBase): void {
|
||||
this.onDidChangeTreeDataEmitter.fire(node);
|
||||
this._onDidChangeTreeDataEmitter.fire(node);
|
||||
}
|
||||
|
||||
public async showNodePicker(expectedContextValue: string): Promise<NodeBase> {
|
||||
let childType: string | undefined = 'Subscription';
|
||||
let quickPicksTask: Promise<uiUtil.PickWithData<NodeBase>[]> = Promise.resolve(this.rootNodes.map((c: NodeBase) => new uiUtil.PickWithData<NodeBase>(c, c.label)));
|
||||
let quickPicksTask: Promise<PickWithData<NodeBase>[]> = Promise.resolve(this._rootNodes.map((c: NodeBase) => new PickWithData<NodeBase>(c, c.label)));
|
||||
|
||||
while (childType) {
|
||||
const pick: uiUtil.PickWithData<NodeBase> = await uiUtil.showQuickPick<NodeBase>(quickPicksTask, localize('azFunc.selectNode', 'Select a {0}', childType));
|
||||
const pick: PickWithData<NodeBase> = await this._ui.showQuickPick<NodeBase>(quickPicksTask, localize('azFunc.selectNode', 'Select a {0}', childType));
|
||||
const node: NodeBase = pick.data;
|
||||
if (node.contextValue === expectedContextValue) {
|
||||
return node;
|
||||
}
|
||||
|
||||
childType = node.childType;
|
||||
quickPicksTask = node.getChildren(false).then((nodes: NodeBase[]): uiUtil.PickWithData<NodeBase>[] => {
|
||||
return nodes.map((c: NodeBase) => new uiUtil.PickWithData<NodeBase>(c, c.label));
|
||||
quickPicksTask = node.getChildren(false).then((nodes: NodeBase[]): PickWithData<NodeBase>[] => {
|
||||
return nodes.map((c: NodeBase) => new PickWithData<NodeBase>(c, c.label));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QuickPickItem } from 'vscode';
|
||||
|
||||
export interface IUserInterface {
|
||||
showQuickPick<T>(items: PickWithData<T>[] | Thenable<PickWithData<T>[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<PickWithData<T>>;
|
||||
showQuickPick(items: Pick[] | Thenable<Pick[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<Pick>;
|
||||
|
||||
showInputBox(placeHolder: string, prompt: string, ignoreFocusOut?: boolean, validateInput?: (s: string) => string | undefined | null, value?: string): Promise<string>;
|
||||
|
||||
showFolderDialog(): Promise<string>;
|
||||
}
|
||||
|
||||
export class Pick implements QuickPickItem {
|
||||
public readonly description: string;
|
||||
public readonly label: string;
|
||||
constructor(label: string, description?: string) {
|
||||
this.label = label;
|
||||
this.description = description ? description : '';
|
||||
}
|
||||
}
|
||||
|
||||
export class PickWithData<T> extends Pick {
|
||||
public readonly data: T;
|
||||
constructor(data: T, label: string, description?: string) {
|
||||
super(label, description);
|
||||
this.data = data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as errors from './errors';
|
||||
import { IUserInterface, Pick, PickWithData } from './IUserInterface';
|
||||
import { localize } from './localize';
|
||||
|
||||
export class VSCodeUI implements IUserInterface {
|
||||
public async showQuickPick<T>(items: PickWithData<T>[] | Thenable<PickWithData<T>[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<PickWithData<T>>;
|
||||
public async showQuickPick(items: Pick[] | Thenable<Pick[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<Pick>;
|
||||
public async showQuickPick(items: vscode.QuickPickItem[] | Thenable<vscode.QuickPickItem[]>, placeHolder: string, ignoreFocusOut: boolean = false): Promise<vscode.QuickPickItem> {
|
||||
const options: vscode.QuickPickOptions = {
|
||||
placeHolder: placeHolder,
|
||||
ignoreFocusOut: ignoreFocusOut
|
||||
};
|
||||
const result: vscode.QuickPickItem | undefined = await vscode.window.showQuickPick(items, options);
|
||||
|
||||
if (!result) {
|
||||
throw new errors.UserCancelledError();
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async showInputBox(placeHolder: string, prompt: string, ignoreFocusOut: boolean = false, validateInput?: (s: string) => string | undefined | null, defaultValue?: string): Promise<string> {
|
||||
const options: vscode.InputBoxOptions = {
|
||||
placeHolder: placeHolder,
|
||||
prompt: prompt,
|
||||
validateInput: validateInput,
|
||||
ignoreFocusOut: ignoreFocusOut,
|
||||
value: defaultValue
|
||||
};
|
||||
const result: string | undefined = await vscode.window.showInputBox(options);
|
||||
|
||||
if (!result) {
|
||||
throw new errors.UserCancelledError();
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async showFolderDialog(): Promise<string> {
|
||||
const defaultUri: vscode.Uri | undefined = vscode.workspace.rootPath ? vscode.Uri.file(vscode.workspace.rootPath) : undefined;
|
||||
const options: vscode.OpenDialogOptions = {
|
||||
defaultUri: defaultUri,
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
openLabel: localize('azFunc.select', 'Select')
|
||||
};
|
||||
const result: vscode.Uri[] | undefined = await vscode.window.showOpenDialog(options);
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
throw new errors.UserCancelledError();
|
||||
} else {
|
||||
return result[0].fsPath;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,14 +3,27 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// tslint:disable-next-line:no-require-imports
|
||||
import CosmosDBManagementClient = require('azure-arm-cosmosdb');
|
||||
import { DatabaseAccount, DatabaseAccountListKeysResult } from 'azure-arm-cosmosdb/lib/models';
|
||||
import * as fs from 'fs';
|
||||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { BaseResource } from 'ms-rest-azure';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { AzureAccount, AzureResourceFilter } from '../azure-account.api';
|
||||
import * as errors from '../errors';
|
||||
import { UserCancelledError } from '../errors';
|
||||
import * as FunctionsCli from '../functions-cli';
|
||||
import { IUserInterface, Pick, PickWithData } from '../IUserInterface';
|
||||
import { localize } from '../localize';
|
||||
import * as uiUtil from '../utils/ui';
|
||||
import { ConfigSetting, ResourceType, ValueType } from '../templates/ConfigSetting';
|
||||
import { Template } from '../templates/Template';
|
||||
import { TemplateData } from '../templates/TemplateData';
|
||||
import * as azUtil from '../utils/azure';
|
||||
import * as fsUtil from '../utils/fs';
|
||||
import * as workspaceUtil from '../utils/workspace';
|
||||
import { VSCodeUI } from '../VSCodeUI';
|
||||
|
||||
const expectedFunctionAppFiles: string[] = [
|
||||
'host.json',
|
||||
|
@ -22,7 +35,7 @@ function getMissingFunctionAppFiles(rootPath: string): string[] {
|
|||
return expectedFunctionAppFiles.filter((file: string) => !fs.existsSync(path.join(rootPath, file)));
|
||||
}
|
||||
|
||||
function validateTemplateName(rootPath: string, name: string): string | undefined {
|
||||
function validateTemplateName(rootPath: string, name: string | undefined): string | undefined {
|
||||
if (!name) {
|
||||
return localize('azFunc.emptyTemplateNameError', 'The template name cannot be empty.');
|
||||
} else if (fs.existsSync(path.join(rootPath, name))) {
|
||||
|
@ -32,9 +45,7 @@ function validateTemplateName(rootPath: string, name: string): string | undefine
|
|||
}
|
||||
}
|
||||
|
||||
export async function createFunction(outputChannel: vscode.OutputChannel): Promise<void> {
|
||||
const functionAppPath: string = await workspaceUtil.selectWorkspaceFolder(localize('azFunc.selectFunctionAppFolderExisting', 'Select the folder containing your function app'));
|
||||
|
||||
async function validateIsFunctionApp(outputChannel: vscode.OutputChannel, functionAppPath: string): Promise<void> {
|
||||
const missingFiles: string[] = getMissingFunctionAppFiles(functionAppPath);
|
||||
if (missingFiles.length !== 0) {
|
||||
const yes: string = localize('azFunc.yes', 'Yes');
|
||||
|
@ -47,20 +58,171 @@ export async function createFunction(outputChannel: vscode.OutputChannel): Promi
|
|||
throw new errors.UserCancelledError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const templates: uiUtil.Pick[] = [
|
||||
new uiUtil.Pick('BlobTrigger'),
|
||||
new uiUtil.Pick('HttpTrigger'),
|
||||
new uiUtil.Pick('QueueTrigger'),
|
||||
new uiUtil.Pick('TimerTrigger')
|
||||
];
|
||||
const template: uiUtil.Pick = await uiUtil.showQuickPick(templates, localize('azFunc.selectFuncTemplate', 'Select a function template'));
|
||||
|
||||
const placeHolder: string = localize('azFunc.funcNamePlaceholder', 'Function Name');
|
||||
async function promptForFunctionName(ui: IUserInterface, functionAppPath: string, template: Template): Promise<string> {
|
||||
const defaultFunctionName: string | undefined = await fsUtil.getUniqueFsPath(functionAppPath, template.defaultFunctionName);
|
||||
const prompt: string = localize('azFunc.funcNamePrompt', 'Provide a function name');
|
||||
const name: string = await uiUtil.showInputBox(placeHolder, prompt, false, (s: string) => validateTemplateName(functionAppPath, s));
|
||||
const placeHolder: string = localize('azFunc.funcNamePlaceholder', 'Function name');
|
||||
|
||||
await FunctionsCli.createFunction(outputChannel, functionAppPath, template.label, name);
|
||||
const newFileUri: vscode.Uri = vscode.Uri.file(path.join(functionAppPath, name, 'index.js'));
|
||||
return await ui.showInputBox(placeHolder, prompt, false, (s: string) => validateTemplateName(functionAppPath, s), defaultFunctionName || template.defaultFunctionName);
|
||||
}
|
||||
|
||||
async function promptForSetting(ui: IUserInterface, azureAccount: AzureAccount, functionAppPath: string, setting: ConfigSetting, defaultValue?: string): Promise<string> {
|
||||
if (setting.resourceType !== undefined) {
|
||||
const subscription: AzureResourceFilter | undefined = await promptForSubscription(ui, azureAccount);
|
||||
|
||||
if (subscription) {
|
||||
const subscriptionId: string | undefined = subscription.subscription.subscriptionId;
|
||||
|
||||
if (subscriptionId) {
|
||||
const credentials: ServiceClientCredentials = subscription.session.credentials;
|
||||
|
||||
switch (setting.resourceType) {
|
||||
case ResourceType.DocumentDB:
|
||||
return await promptForDocumentDB(ui, functionAppPath, credentials, subscriptionId, setting);
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (setting.valueType) {
|
||||
case ValueType.boolean:
|
||||
return await promptForBooleanSetting(ui, setting);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 'string' type for any setting that isn't supported
|
||||
return await promptForStringSetting(ui, setting, defaultValue);
|
||||
}
|
||||
|
||||
async function promptForSubscription(ui: IUserInterface, azureAccount: AzureAccount): Promise<AzureResourceFilter | undefined> {
|
||||
if (azureAccount.filters.length === 0) {
|
||||
return undefined;
|
||||
} else if (azureAccount.filters.length === 1) {
|
||||
return azureAccount.filters[0];
|
||||
} else {
|
||||
const subscriptionPicks: PickWithData<AzureResourceFilter>[] = [];
|
||||
azureAccount.filters.forEach((f: AzureResourceFilter) => {
|
||||
const subscriptionId: string | undefined = f.subscription.subscriptionId;
|
||||
if (subscriptionId) {
|
||||
const label: string = f.subscription.displayName || subscriptionId;
|
||||
subscriptionPicks.push(new PickWithData<AzureResourceFilter>(f, label, subscriptionId));
|
||||
}
|
||||
});
|
||||
const placeHolder: string = localize('azFunc.selectSubscription', 'Select a Subscription');
|
||||
|
||||
return (await ui.showQuickPick<AzureResourceFilter>(subscriptionPicks, placeHolder)).data;
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForDocumentDB(ui: IUserInterface, functionAppPath: string, credentials: ServiceClientCredentials, subscriptionId: string, setting: ConfigSetting): Promise<string> {
|
||||
const client: CosmosDBManagementClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const dbAccount: DatabaseAccount = await showResourceQuickPick<DatabaseAccount>(ui, setting, client.databaseAccounts.list());
|
||||
if (dbAccount.id && dbAccount.name) {
|
||||
const resourceGroup: string = azUtil.getResourceGroupFromId(dbAccount.id);
|
||||
const keys: DatabaseAccountListKeysResult = await client.databaseAccounts.listKeys(resourceGroup, dbAccount.name);
|
||||
const appSettingName: string = `${dbAccount.name}_${ResourceType.DocumentDB.toUpperCase()}`;
|
||||
await setLocalFunctionAppSetting(functionAppPath, appSettingName, `AccountEndpoint=${dbAccount.documentEndpoint};AccountKey=${keys.primaryMasterKey};`);
|
||||
|
||||
return appSettingName;
|
||||
} else {
|
||||
throw new Error(localize('azFunc.InvalidCosmosDBAccount', 'Invalid Cosmos DB Account'));
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForBooleanSetting(ui: IUserInterface, setting: ConfigSetting): Promise<string> {
|
||||
const picks: Pick[] = [new Pick('true'), new Pick('false')];
|
||||
|
||||
return (await ui.showQuickPick(picks, setting.label, false)).label;
|
||||
}
|
||||
|
||||
async function promptForStringSetting(ui: IUserInterface, setting: ConfigSetting, defaultValue?: string): Promise<string> {
|
||||
const prompt: string = localize('azFunc.stringSettingPrompt', 'Provide a \'{0}\'', setting.label);
|
||||
defaultValue = defaultValue ? defaultValue : setting.defaultValue;
|
||||
|
||||
return await ui.showInputBox(setting.label, prompt, false, (s: string) => setting.validateSetting(s), defaultValue);
|
||||
}
|
||||
|
||||
interface IBaseResourceWithName extends BaseResource {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
async function showResourceQuickPick<T extends IBaseResourceWithName>(ui: IUserInterface, setting: ConfigSetting, resourcesTask: Promise<T[]>): Promise<T> {
|
||||
const picksTask: Promise<PickWithData<T>[]> = resourcesTask.then((resources: T[]) => {
|
||||
return <PickWithData<T>[]>(resources
|
||||
.map((br: T) => br.name ? new PickWithData(br, br.name) : undefined)
|
||||
.filter((p: PickWithData<T> | undefined) => p));
|
||||
});
|
||||
const prompt: string = localize('azFunc.resourcePrompt', 'Select a \'{0}\'', setting.label);
|
||||
|
||||
return (await ui.showQuickPick<T>(picksTask, prompt)).data;
|
||||
}
|
||||
|
||||
interface ILocalSettings {
|
||||
Values: { [key: string]: string };
|
||||
}
|
||||
|
||||
async function setLocalFunctionAppSetting(functionAppPath: string, key: string, value: string): Promise<void> {
|
||||
const localSettingsPath: string = path.join(functionAppPath, 'local.settings.json');
|
||||
const localSettings: ILocalSettings = <ILocalSettings>JSON.parse(await fsUtil.readFromFile(localSettingsPath));
|
||||
if (localSettings.Values[key]) {
|
||||
const message: string = localize('azFunc.SettingAlreadyExists', 'Local app setting \'{0}\' already exists. Overwrite?', key);
|
||||
const yes: string = localize('azFunc.yes', 'Yes');
|
||||
if (await vscode.window.showWarningMessage(message, yes) !== yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
localSettings.Values[key] = value;
|
||||
await fsUtil.writeJsonToFile(localSettingsPath, localSettings);
|
||||
}
|
||||
|
||||
export async function createFunction(
|
||||
outputChannel: vscode.OutputChannel,
|
||||
azureAccount: AzureAccount,
|
||||
templateData: TemplateData,
|
||||
ui: IUserInterface = new VSCodeUI()): Promise<void> {
|
||||
|
||||
const folderPlaceholder: string = localize('azFunc.selectFunctionAppFolderExisting', 'Select the folder containing your function app');
|
||||
const functionAppPath: string = await workspaceUtil.selectWorkspaceFolder(ui, folderPlaceholder);
|
||||
await validateIsFunctionApp(outputChannel, functionAppPath);
|
||||
|
||||
const templatePicks: PickWithData<Template>[] = (await templateData.getTemplates()).map((t: Template) => new PickWithData<Template>(t, t.name));
|
||||
const templatePlaceHolder: string = localize('azFunc.selectFuncTemplate', 'Select a function template');
|
||||
const template: Template = (await ui.showQuickPick<Template>(templatePicks, templatePlaceHolder)).data;
|
||||
|
||||
const name: string = await promptForFunctionName(ui, functionAppPath, template);
|
||||
let showPrompts: boolean = true;
|
||||
for (const settingName of template.userPromptedSettings) {
|
||||
const setting: ConfigSetting | undefined = await templateData.getSetting(template.bindingType, settingName);
|
||||
if (setting) {
|
||||
try {
|
||||
let settingValue: string | undefined;
|
||||
const defaultValue: string | undefined = template.getSetting(settingName);
|
||||
if (showPrompts) {
|
||||
settingValue = await promptForSetting(ui, azureAccount, functionAppPath, setting, defaultValue);
|
||||
} else {
|
||||
settingValue = defaultValue;
|
||||
}
|
||||
|
||||
template.setSetting(settingName, settingValue);
|
||||
} catch (error) {
|
||||
if (error instanceof UserCancelledError) {
|
||||
const message: string = localize('azFunc.IncompleteFunction', 'Function \'{0}\' was created, but you must finish specifying settings in \'function.json\'.', name);
|
||||
vscode.window.showWarningMessage(message);
|
||||
showPrompts = false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const functionPath: string = path.join(functionAppPath, name);
|
||||
await template.writeTemplateFiles(functionPath);
|
||||
|
||||
const newFileUri: vscode.Uri = vscode.Uri.file(path.join(functionPath, 'index.js'));
|
||||
vscode.window.showTextDocument(await vscode.workspace.openTextDocument(newFileUri));
|
||||
}
|
||||
|
|
|
@ -7,13 +7,15 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as FunctionsCli from '../functions-cli';
|
||||
import { IUserInterface } from '../IUserInterface';
|
||||
import { localize } from '../localize';
|
||||
import * as TemplateFiles from '../template-files';
|
||||
import * as fsUtil from '../utils/fs';
|
||||
import * as workspaceUtil from '../utils/workspace';
|
||||
import { VSCodeUI } from '../VSCodeUI';
|
||||
|
||||
export async function createNewProject(outputChannel: vscode.OutputChannel): Promise<void> {
|
||||
const functionAppPath: string = await workspaceUtil.selectWorkspaceFolder(localize('azFunc.selectFunctionAppFolderNew', 'Select the folder that will contain your function app'));
|
||||
export async function createNewProject(outputChannel: vscode.OutputChannel, ui: IUserInterface = new VSCodeUI()): Promise<void> {
|
||||
const functionAppPath: string = await workspaceUtil.selectWorkspaceFolder(ui, localize('azFunc.selectFunctionAppFolderNew', 'Select the folder that will contain your function app'));
|
||||
|
||||
const tasksJsonPath: string = path.join(functionAppPath, '.vscode', 'tasks.json');
|
||||
const tasksJsonExists: boolean = fs.existsSync(tasksJsonPath);
|
||||
|
@ -23,8 +25,8 @@ export async function createNewProject(outputChannel: vscode.OutputChannel): Pro
|
|||
await FunctionsCli.createNewProject(outputChannel, functionAppPath);
|
||||
|
||||
if (!tasksJsonExists && !launchJsonExists) {
|
||||
await fsUtil.writeToFile(tasksJsonPath, TemplateFiles.getTasksJson());
|
||||
await fsUtil.writeToFile(launchJsonPath, TemplateFiles.getLaunchJson());
|
||||
await fsUtil.writeJsonToFile(tasksJsonPath, TemplateFiles.tasksJson);
|
||||
await fsUtil.writeJsonToFile(launchJsonPath, TemplateFiles.launchJson);
|
||||
}
|
||||
|
||||
if (!workspaceUtil.isFolderOpenInWorkspace(functionAppPath)) {
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
import WebSiteManagementClient = require('azure-arm-website');
|
||||
import * as vscode from 'vscode';
|
||||
import { AzureFunctionsExplorer } from '../AzureFunctionsExplorer';
|
||||
import { IUserInterface } from '../IUserInterface';
|
||||
import { localize } from '../localize';
|
||||
import { FunctionAppNode } from '../nodes/FunctionAppNode';
|
||||
import * as workspaceUtil from '../utils/workspace';
|
||||
import { VSCodeUI } from '../VSCodeUI';
|
||||
|
||||
export async function deployZip(explorer: AzureFunctionsExplorer, outputChannel: vscode.OutputChannel, uri?: vscode.Uri, node?: FunctionAppNode): Promise<void> {
|
||||
const folderPath: string = uri ? uri.fsPath : await workspaceUtil.selectWorkspaceFolder(localize('azFunc.selectZipDeployFolder', 'Select the folder to zip and deploy'));
|
||||
export async function deployZip(explorer: AzureFunctionsExplorer, outputChannel: vscode.OutputChannel, uri?: vscode.Uri, node?: FunctionAppNode, ui: IUserInterface = new VSCodeUI()): Promise<void> {
|
||||
const folderPath: string = uri ? uri.fsPath : await workspaceUtil.selectWorkspaceFolder(ui, localize('azFunc.selectZipDeployFolder', 'Select the folder to zip and deploy'));
|
||||
|
||||
if (!node) {
|
||||
node = <FunctionAppNode>(await explorer.showNodePicker(FunctionAppNode.contextValue));
|
||||
|
|
|
@ -21,6 +21,7 @@ import * as errors from './errors';
|
|||
import { localize } from './localize';
|
||||
import { FunctionAppNode } from './nodes/FunctionAppNode';
|
||||
import { NodeBase } from './nodes/NodeBase';
|
||||
import { TemplateData } from './templates/TemplateData';
|
||||
|
||||
let reporter: TelemetryReporter | undefined;
|
||||
|
||||
|
@ -47,9 +48,11 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||
context.subscriptions.push(azureAccount.onFiltersChanged(() => explorer.refresh()));
|
||||
context.subscriptions.push(azureAccount.onStatusChanged(() => explorer.refresh()));
|
||||
|
||||
const templateData: TemplateData = new TemplateData(context.globalState);
|
||||
|
||||
initCommand<NodeBase>(context, 'azureFunctions.refresh', (node?: NodeBase) => explorer.refresh(node));
|
||||
initCommand<NodeBase>(context, 'azureFunctions.openInPortal', async (node?: NodeBase) => await openInPortal(explorer, node));
|
||||
initAsyncCommand<NodeBase>(context, 'azureFunctions.createFunction', async () => await createFunction(outputChannel));
|
||||
initAsyncCommand<NodeBase>(context, 'azureFunctions.createFunction', async () => await createFunction(outputChannel, azureAccount, templateData));
|
||||
initAsyncCommand<NodeBase>(context, 'azureFunctions.createNewProject', async () => await createNewProject(outputChannel));
|
||||
initAsyncCommand<NodeBase>(context, 'azureFunctions.startFunctionApp', async (node?: FunctionAppNode) => await startFunctionApp(explorer, node));
|
||||
initAsyncCommand<NodeBase>(context, 'azureFunctions.stopFunctionApp', async (node?: FunctionAppNode) => await stopFunctionApp(explorer, node));
|
||||
|
|
|
@ -7,10 +7,7 @@ import * as cp from 'child_process';
|
|||
import * as vscode from 'vscode';
|
||||
import { localize } from './localize';
|
||||
|
||||
export async function createFunction(outputChannel: vscode.OutputChannel, workingDirectory: string, templateName: string, name: string): Promise<void> {
|
||||
await executeCommand(outputChannel, workingDirectory, 'new', '--language', 'JavaScript', '--template', templateName, '--name', name);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:export-name
|
||||
export async function createNewProject(outputChannel: vscode.OutputChannel, workingDirectory: string): Promise<void> {
|
||||
await executeCommand(outputChannel, workingDirectory, 'init');
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { localize } from './localize';
|
||||
|
||||
const taskId: string = 'launchFunctionApp';
|
||||
const tasksJson: {} = {
|
||||
export const tasksJson: {} = {
|
||||
version: '2.0.0',
|
||||
tasks: [
|
||||
{
|
||||
|
@ -40,7 +40,7 @@ const tasksJson: {} = {
|
|||
]
|
||||
};
|
||||
|
||||
const launchJson: {} = {
|
||||
export const launchJson: {} = {
|
||||
version: '0.2.0',
|
||||
configurations: [
|
||||
{
|
||||
|
@ -53,15 +53,3 @@ const launchJson: {} = {
|
|||
}
|
||||
]
|
||||
};
|
||||
|
||||
function stringifyJSON(data: {}): string {
|
||||
return JSON.stringify(data, null, ' ');
|
||||
}
|
||||
|
||||
export function getTasksJson(): string {
|
||||
return stringifyJSON(tasksJson);
|
||||
}
|
||||
|
||||
export function getLaunchJson(): string {
|
||||
return stringifyJSON(launchJson);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ConfigBinding } from './ConfigBinding';
|
||||
import { ConfigVariables } from './ConfigVariables';
|
||||
import { Resources } from './Resources';
|
||||
|
||||
interface IConfig {
|
||||
variables: { [name: string]: string };
|
||||
bindings: object[];
|
||||
}
|
||||
|
||||
export class Config {
|
||||
public bindings: ConfigBinding[];
|
||||
constructor(data: object, resources: Resources) {
|
||||
const config: IConfig = <IConfig>data;
|
||||
const variables: ConfigVariables = new ConfigVariables(config.variables, resources);
|
||||
this.bindings = config.bindings.map((b: object) => new ConfigBinding(variables, b));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ConfigSetting } from './ConfigSetting';
|
||||
import { ConfigVariables } from './ConfigVariables';
|
||||
|
||||
interface IBinding {
|
||||
// tslint:disable-next-line:no-reserved-keywords
|
||||
type: string;
|
||||
settings: object[];
|
||||
}
|
||||
|
||||
export class ConfigBinding {
|
||||
public bindingType: string;
|
||||
public settings: ConfigSetting[];
|
||||
constructor(variables: ConfigVariables, data: object) {
|
||||
const binding: IBinding = <IBinding>data;
|
||||
this.bindingType = binding.type;
|
||||
this.settings = binding.settings.map((s: object) => new ConfigSetting(variables, s));
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ConfigValidator } from './ConfigValidator';
|
||||
import { ConfigVariables } from './ConfigVariables';
|
||||
|
||||
export enum ValueType {
|
||||
string = 'string',
|
||||
boolean = 'boolean',
|
||||
enum = 'enum',
|
||||
checkBoxList = 'checkBoxList'
|
||||
}
|
||||
|
||||
export enum ResourceType {
|
||||
DocumentDB = 'DocumentDB'
|
||||
}
|
||||
|
||||
interface IBindingSetting {
|
||||
name: string;
|
||||
value: ValueType;
|
||||
label: string;
|
||||
defaultValue?: string;
|
||||
required: boolean;
|
||||
resource?: ResourceType;
|
||||
validators?: object[];
|
||||
}
|
||||
|
||||
export class ConfigSetting {
|
||||
private _setting: IBindingSetting;
|
||||
private _variables: ConfigVariables;
|
||||
constructor(variables: ConfigVariables, data: object) {
|
||||
this._variables = variables;
|
||||
this._setting = <IBindingSetting>data;
|
||||
}
|
||||
|
||||
public get resourceType(): ResourceType | undefined {
|
||||
return this._setting.resource;
|
||||
}
|
||||
|
||||
public get valueType(): ValueType | undefined {
|
||||
return this._setting.value;
|
||||
}
|
||||
|
||||
public get defaultValue(): string | undefined {
|
||||
return this._setting.defaultValue ? this._variables.getValue(this._setting.defaultValue) : undefined;
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
return this._variables.getValue(this._setting.label);
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this._variables.getValue(this._setting.name);
|
||||
}
|
||||
|
||||
public validateSetting(value: string | undefined): string | undefined {
|
||||
if (this._setting.validators) {
|
||||
const validators: ConfigValidator[] = this._setting.validators.map((v: object) => new ConfigValidator(this._variables, v));
|
||||
for (const validator of validators) {
|
||||
if (!value || value.match(validator.expression) === null) {
|
||||
return validator.errorText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ConfigVariables } from './ConfigVariables';
|
||||
|
||||
interface IConfigValidator {
|
||||
expression: string;
|
||||
errorText: string;
|
||||
}
|
||||
|
||||
export class ConfigValidator {
|
||||
private _validator: IConfigValidator;
|
||||
private _variables: ConfigVariables;
|
||||
constructor(variables: ConfigVariables, data: object) {
|
||||
this._variables = variables;
|
||||
this._validator = <IConfigValidator>data;
|
||||
}
|
||||
|
||||
public get expression(): string {
|
||||
return this._validator.expression;
|
||||
}
|
||||
|
||||
public get errorText(): string | undefined {
|
||||
return this._variables.getValue(this._validator.errorText);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Resources } from './Resources';
|
||||
|
||||
export class ConfigVariables {
|
||||
private _variables: { [name: string]: string };
|
||||
private _resources: Resources;
|
||||
constructor(variables: { [name: string]: string }, resources: Resources) {
|
||||
this._variables = variables;
|
||||
this._resources = resources;
|
||||
}
|
||||
|
||||
public getValue(data: string): string {
|
||||
const matches: RegExpMatchArray | null = data.match(/\[variables\(\'(.*)\'\)\]/);
|
||||
data = matches !== null ? this._variables[matches[1]] : data;
|
||||
|
||||
return this._resources.getValue(data);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
# Azure Function Templates
|
||||
|
||||
The code in this folder is used to parse and model the [function templates](https://github.com/Azure/azure-webjobs-sdk-templates) provided by the [Azure Functions portal](https://functions.azure.com/signin). The portal provides template data in three parts: templates.json, bindingconfig.json, and resources.json. See below for an example of the schema for the TimerTrigger template
|
||||
|
||||
## Templates.json
|
||||
[https://functions.azure.com/api/templates?runtime=~2](https://functions.azure.com/api/templates?runtime=~2)
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "TimerTrigger-JavaScript",
|
||||
"function": {
|
||||
"disabled": false,
|
||||
"bindings": [
|
||||
{
|
||||
"name": "myTimer",
|
||||
"type": "timerTrigger",
|
||||
"direction": "in",
|
||||
"schedule": "0 */5 * * * *"
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"defaultFunctionName": "TimerTriggerJS",
|
||||
"description": "$TimerTriggerNodeJS_description",
|
||||
"name": "TimerTrigger",
|
||||
"language": "JavaScript",
|
||||
"category": [
|
||||
"$temp_category_core",
|
||||
"$temp_category_dataProcessing"
|
||||
],
|
||||
"enabledInTryMode": true,
|
||||
"userPrompt": [
|
||||
"schedule"
|
||||
]
|
||||
},
|
||||
"files": {
|
||||
"index.js": "module.exports = function (context, myTimer) {\n var timeStamp = new Date().toISOString();\n \n if(myTimer.isPastDue)\n {\n context.log('JavaScript is running late!');\n }\n context.log('JavaScript timer trigger function ran!', timeStamp); \n \n context.done();\n};",
|
||||
"readme.md": "# TimerTrigger - JavaScript\n\nThe `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.\n\n## How it works\n\nFor a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: \"When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year\".\n\n## Learn more\n\n<TODO> Documentation",
|
||||
"sample.dat": ""
|
||||
},
|
||||
"runtime": "default"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## BindingConfig.json
|
||||
[https://functions.azure.com/api/bindingconfig?runtime=~2](https://functions.azure.com/api/bindingconfig?runtime=~2)
|
||||
```json
|
||||
{
|
||||
"$schema": "<TBD>",
|
||||
"contentVersion": "2016-03-04-alpha",
|
||||
"variables": {
|
||||
"parameterName": "$variables_parameterName"
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"type": "timerTrigger",
|
||||
"displayName": "$timerTrigger_displayName",
|
||||
"direction": "trigger",
|
||||
"enabledInTryMode": true,
|
||||
"documentation": "## Settings for timer trigger\n\nThe settings provide a schedule expression. For example, the following schedule runs the function every minute:\n\n - `schedule`: Cron tab expression which defines schedule \n - `name`: The variable name used in function code for the TimerTrigger. \n - `type`: must be *timerTrigger*\n - `direction`: must be *in*\n\nThe timer trigger handles multi-instance scale-out automatically: only a single instance of a particular timer function will be running across all instances.\n\n## Format of schedule expression\n\nThe schedule expression is a [CRON expression](http://en.wikipedia.org/wiki/Cron#CRON_expression) that includes 6 fields: `{second} {minute} {hour} {day} {month} {day of the week}`. \n\nNote that many of the cron expressions you find online omit the {second} field, so if you copy from one of those you'll have to adjust for the extra field. \n\nHere are some other schedule expression examples:\n\nTo trigger once every 5 minutes:\n\n```json\n\"schedule\": \"0 */5 * * * *\"\n```\n\nTo trigger once at the top of every hour:\n\n```json\n\"schedule\": \"0 0 * * * *\",\n```\n\nTo trigger once every two hours:\n\n```json\n\"schedule\": \"0 0 */2 * * *\",\n```\n\nTo trigger once every hour from 9 AM to 5 PM:\n\n```json\n\"schedule\": \"0 0 9-17 * * *\",\n```\n\nTo trigger At 9:30 AM every day:\n\n```json\n\"schedule\": \"0 30 9 * * *\",\n```\n\nTo trigger At 9:30 AM every weekday:\n\n```json\n\"schedule\": \"0 30 9 * * 1-5\",\n```\n\n## Timer trigger C# code example\n\nThis C# code example writes a single log each time the function is triggered.\n\n```csharp\npublic static void Run(TimerInfo myTimer, TraceWriter log)\n{\n log.Info($\"C# Timer trigger function executed at: {DateTime.Now}\"); \n}\n```\n\n## Timer trigger JavaScript example\n\n```JavaScript\nmodule.exports = function(context, myTimer) {\n if(myTimer.isPastDue)\n {\n context.log('JavaScript is running late!');\n }\n context.log(\"Timer last triggered at \" + myTimer.last);\n context.log(\"Timer triggered at \" + myTimer.next);\n \n context.done();\n}\n```",
|
||||
"settings": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "string",
|
||||
"defaultValue": "myTimer",
|
||||
"required": true,
|
||||
"label": "$timerTrigger_name_label",
|
||||
"help": "$timerTrigger_name_help",
|
||||
"validators": [
|
||||
{
|
||||
"expression": "^[a-zA-Z][a-zA-Z0-9]{0,127}$",
|
||||
"errorText": "[variables('parameterName')]"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "schedule",
|
||||
"value": "string",
|
||||
"defaultValue": "0 * * * * *",
|
||||
"required": true,
|
||||
"label": "$timerTrigger_schedule_label",
|
||||
"help": "$timerTrigger_schedule_help",
|
||||
"validators": [
|
||||
{
|
||||
"expression": "^(\\*|((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?)(,((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?)(,((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|(((1\\d)|(2[0-3])|\\d)(\\-((1\\d)|(2[0-3])|\\d)(\\/\\d+)?)?)(,(((1\\d)|(2[0-3])|\\d)(\\-((1\\d)|(2[0-3])|\\d)(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|((([1-2]\\d)|(3[0-1])|[1-9])(\\-(([1-2]\\d)|(3[0-1])|[1-9])(\\/\\d+)?)?)(,((([1-2]\\d)|(3[0-1])|[1-9])(\\-(([1-2]\\d)|(3[0-1])|[1-9])(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|(([A-Za-z]+|(1[0-2])|[1-9])(\\-([A-Za-z]+|(1[0-2])|[1-9])(\\/\\d+)?)?)(,(([A-Za-z]+|(1[0-2])|[1-9])(\\-([A-Za-z]+|(1[0-2])|[1-9])(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|(([A-Za-z]+|[0-6])(\\-([A-Za-z]+|[0-6])(\\/\\d+)?)?)(,(([A-Za-z]+|[0-6])(\\-([A-Za-z]+|[0-6])(\\/\\d+)?)?))*)(\\/\\d+)?$",
|
||||
"errorText": "$timerTrigger_schedule_errorText"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Resources.json
|
||||
[https://functions.azure.com/api/resources?runtime=~2&name=en-us](https://functions.azure.com/api/resources?runtime=~2&name=en-us)
|
||||
```json
|
||||
{
|
||||
"lang": {},
|
||||
"en": {
|
||||
"temp_category_core": "Core",
|
||||
"temp_category_dataProcessing": "Data Processing",
|
||||
"timerTrigger_displayName": "Timer",
|
||||
"timerTrigger_name_help": "The name used to identify this trigger in your code",
|
||||
"timerTrigger_name_label": "Timestamp parameter name",
|
||||
"timerTrigger_schedule_help": "Enter a cron expression of the format '{second} {minute} {hour} {day} {month} {day of week}' to specify the schedule. See documentation below for examples.",
|
||||
"timerTrigger_schedule_label": "Schedule",
|
||||
"TimerTriggerNodeJS_description": "A JavaScript function that will be run on a specified schedule",
|
||||
"timerTrigger_schedule_errorText": "Invalid Cron Expression. Please consult the <a target='_blank' href='https://azure.microsoft.com/en-us/documentation/articles/functions-bindings-timer/'>documentation</a> to learn more.",
|
||||
"variables_parameterName": "The parameter name must be an alphanumeric string of any number of characters and cannot start with a number."
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface IResources {
|
||||
en: { [key: string]: string };
|
||||
}
|
||||
|
||||
export class Resources {
|
||||
private _resources: IResources;
|
||||
constructor(data: object) {
|
||||
this._resources = <IResources>data;
|
||||
}
|
||||
|
||||
public getValue(data: string): string {
|
||||
const matches: RegExpMatchArray | null = data.match(/\$(.*)/);
|
||||
|
||||
return matches !== null ? this._resources.en[matches[1]] : data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fsUtil from '../utils/fs';
|
||||
import { Resources } from './Resources';
|
||||
|
||||
interface ITemplate {
|
||||
id: string;
|
||||
// tslint:disable-next-line:no-reserved-keywords
|
||||
function: ITemplateFunction;
|
||||
metadata: ITemplateMetadata;
|
||||
files: { [filename: string]: string };
|
||||
}
|
||||
|
||||
interface ITemplateFunction {
|
||||
disabled: boolean;
|
||||
bindings: { [propertyName: string]: string }[];
|
||||
}
|
||||
|
||||
interface ITemplateMetadata {
|
||||
defaultFunctionName: string;
|
||||
name: string;
|
||||
language: TemplateLanguage;
|
||||
userPrompt?: string[];
|
||||
category: TemplateCategory[];
|
||||
}
|
||||
|
||||
export enum TemplateLanguage {
|
||||
JavaScript = 'JavaScript'
|
||||
}
|
||||
|
||||
export enum TemplateCategory {
|
||||
Core = '$temp_category_core'
|
||||
}
|
||||
|
||||
export class Template {
|
||||
private _template: ITemplate;
|
||||
private _resources: Resources;
|
||||
constructor(template: object, resources: Resources) {
|
||||
this._template = <ITemplate>template;
|
||||
this._resources = resources;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this._resources.getValue(this._template.metadata.name);
|
||||
}
|
||||
|
||||
public get defaultFunctionName(): string {
|
||||
return this._template.metadata.defaultFunctionName;
|
||||
}
|
||||
|
||||
public get language(): TemplateLanguage {
|
||||
return this._template.metadata.language;
|
||||
}
|
||||
|
||||
public isCategory(category: TemplateCategory): boolean {
|
||||
return this._template.metadata.category.find((c: TemplateCategory) => c === category) !== undefined;
|
||||
}
|
||||
|
||||
public get bindingType(): string {
|
||||
// The first binding is the 'input' binding that matters for userInput
|
||||
return this._template.function.bindings[0].type;
|
||||
}
|
||||
|
||||
public get userPromptedSettings(): string[] {
|
||||
return this._template.metadata.userPrompt ? this._template.metadata.userPrompt : [];
|
||||
}
|
||||
|
||||
public getSetting(name: string): string | undefined {
|
||||
// The first binding is the 'input' binding that matters for userInput
|
||||
return this._template.function.bindings[0][name];
|
||||
}
|
||||
|
||||
public setSetting(name: string, value?: string): void {
|
||||
// The first binding is the 'input' binding that matters for userInput
|
||||
this._template.function.bindings[0][name] = value ? value : '';
|
||||
}
|
||||
|
||||
public async writeTemplateFiles(functionPath: string): Promise<void> {
|
||||
await fsUtil.makeFolder(functionPath);
|
||||
const tasks: Promise<void>[] = Object.keys(this._template.files).map(async (fileName: string) => {
|
||||
await fsUtil.writeToFile(path.join(functionPath, fileName), this._template.files[fileName]);
|
||||
});
|
||||
|
||||
tasks.push(fsUtil.writeJsonToFile(path.join(functionPath, 'function.json'), this._template.function));
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// tslint:disable-next-line:no-require-imports
|
||||
import request = require('request-promise');
|
||||
import * as vscode from 'vscode';
|
||||
import { localize } from '../localize';
|
||||
import { Config } from './Config';
|
||||
import { ConfigBinding } from './ConfigBinding';
|
||||
import { ConfigSetting } from './ConfigSetting';
|
||||
import { Resources } from './Resources';
|
||||
import { Template, TemplateCategory, TemplateLanguage } from './Template';
|
||||
|
||||
/**
|
||||
* Main container for all template data retrieved from the Azure Functions Portal. See README.md for more info and example of the schema.
|
||||
* We cache the template data retrieved from the portal so that the user can create functions offline.
|
||||
*/
|
||||
export class TemplateData {
|
||||
private readonly _templateInitError: Error = new Error(localize('azFunc.TemplateInitError', 'Failed to retrieve templates from the Azure Functions Portal.'));
|
||||
private readonly _templatesKey: string = 'FunctionTemplates';
|
||||
private readonly _configKey: string = 'FunctionTemplateConfig';
|
||||
private readonly _resourcesKey: string = 'FunctionTemplateResources';
|
||||
private readonly _refreshTask: Promise<void>;
|
||||
private _templates: Template[] | undefined;
|
||||
private _config: Config | undefined;
|
||||
|
||||
constructor(globalState?: vscode.Memento) {
|
||||
if (globalState) {
|
||||
const cachedResources: object | undefined = globalState.get<object>(this._resourcesKey);
|
||||
const cachedTemplates: object[] | undefined = globalState.get<object[]>(this._templatesKey);
|
||||
const cachedConfig: object | undefined = globalState.get<object>(this._configKey);
|
||||
|
||||
if (cachedResources && cachedTemplates && cachedConfig) {
|
||||
const resources: Resources = new Resources(cachedResources);
|
||||
this._templates = cachedTemplates.map((t: object) => new Template(t, resources));
|
||||
this._config = new Config(cachedConfig, resources);
|
||||
}
|
||||
}
|
||||
|
||||
this._refreshTask = this.refreshTemplates(globalState);
|
||||
}
|
||||
|
||||
public async getTemplates(): Promise<Template[]> {
|
||||
if (this._templates === undefined) {
|
||||
await this._refreshTask;
|
||||
if (this._templates === undefined) {
|
||||
throw this._templateInitError;
|
||||
}
|
||||
}
|
||||
|
||||
return this._templates.filter((t: Template) => {
|
||||
return t.language === TemplateLanguage.JavaScript && t.isCategory(TemplateCategory.Core);
|
||||
});
|
||||
}
|
||||
|
||||
public async getSetting(bindingType: string, settingName: string): Promise<ConfigSetting | undefined> {
|
||||
if (this._config === undefined) {
|
||||
await this._refreshTask;
|
||||
if (this._config === undefined) {
|
||||
throw this._templateInitError;
|
||||
}
|
||||
}
|
||||
|
||||
const binding: ConfigBinding | undefined = this._config.bindings.find((b: ConfigBinding) => b.bindingType === bindingType);
|
||||
if (binding) {
|
||||
return binding.settings.find((bs: ConfigSetting) => bs.name === settingName);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshTemplates(globalState?: vscode.Memento): Promise<void> {
|
||||
try {
|
||||
const rawResources: object = await this.requestFunctionPortal<object>('resources', 'name=en-us');
|
||||
const rawTemplates: object[] = await this.requestFunctionPortal<object[]>('templates');
|
||||
const rawConfig: object = await this.requestFunctionPortal<object>('bindingconfig');
|
||||
|
||||
const resources: Resources = new Resources(rawResources);
|
||||
this._templates = rawTemplates.map((t: object) => new Template(t, resources));
|
||||
this._config = new Config(rawConfig, resources);
|
||||
|
||||
if (globalState) {
|
||||
globalState.update(this._templatesKey, rawTemplates);
|
||||
globalState.update(this._configKey, rawConfig);
|
||||
globalState.update(this._resourcesKey, rawResources);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore errors - use cached version of templates instead
|
||||
}
|
||||
}
|
||||
|
||||
private async requestFunctionPortal<T>(subPath: string, param?: string): Promise<T> {
|
||||
const options: request.OptionsWithUri = {
|
||||
method: 'GET',
|
||||
uri: `https://functions.azure.com/api/${subPath}?runtime=~2$&${param}`,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0' // Required otherwise we get Unauthorized
|
||||
}
|
||||
};
|
||||
|
||||
return <T>(JSON.parse(await <Thenable<string>>request(options).promise()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from '../localize';
|
||||
|
||||
// tslint:disable-next-line:export-name
|
||||
export function getResourceGroupFromId(id: string): string {
|
||||
const matches: RegExpMatchArray | null = id.match(/\/subscriptions\/(.*)\/resourceGroups\/(.*)\/providers\/(.*)\/(.*)/);
|
||||
|
||||
if (matches === null || matches.length < 3) {
|
||||
throw new Error(localize('azFunc.InvalidResourceId', 'Invalid Azure Resource Id'));
|
||||
}
|
||||
|
||||
return matches[2];
|
||||
}
|
|
@ -4,11 +4,17 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
// tslint:disable-next-line:no-require-imports
|
||||
import rimraf = require('rimraf');
|
||||
|
||||
// tslint:disable-next-line:export-name
|
||||
export async function writeToFile(path: string, data: string): Promise<void> {
|
||||
export async function writeJsonToFile(fsPath: string, data: object): Promise<void> {
|
||||
await writeToFile(fsPath, JSON.stringify(data, undefined, 2));
|
||||
}
|
||||
|
||||
export async function writeToFile(fsPath: string, data: string): Promise<void> {
|
||||
await new Promise((resolve: () => void, reject: (e: Error) => void): void => {
|
||||
fs.writeFile(path, data, (error?: Error) => {
|
||||
fs.writeFile(fsPath, data, (error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
|
@ -17,3 +23,73 @@ export async function writeToFile(path: string, data: string): Promise<void> {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function readFromFile(fsPath: string): Promise<string> {
|
||||
return await new Promise((resolve: (data: string) => void, reject: (e: Error) => void): void => {
|
||||
fs.readFile(fsPath, (error: Error | undefined, data: Buffer) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(data.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function makeFolder(fsPath: string): Promise<void> {
|
||||
if (!(await fsPathExists(fsPath))) {
|
||||
await new Promise((resolve: () => void, reject: (err: Error) => void): void => {
|
||||
fs.mkdir(fsPath, (err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFolderAndContents(fsPath: string): Promise<void> {
|
||||
await new Promise((resolve: () => void, reject: (err: Error) => void): void => {
|
||||
rimraf(fsPath, (err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function fsPathExists(fsPath: string): Promise<boolean> {
|
||||
return await new Promise((resolve: (r: boolean) => void, reject: (e: Error) => void): void => {
|
||||
fs.exists(fsPath, (result: boolean, error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUniqueFsPath(folderPath: string, defaultValue: string): Promise<string | undefined> {
|
||||
let count: number = 0;
|
||||
const maxCount: number = 1024;
|
||||
|
||||
while (count < maxCount) {
|
||||
const fileName: string = defaultValue + (count === 0 ? '' : count.toString());
|
||||
if (!(await fsPathExists(path.join(folderPath, fileName)))) {
|
||||
return fileName;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function randomName(): string {
|
||||
// tslint:disable-next-line:insecure-random
|
||||
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { QuickPickItem } from 'vscode';
|
||||
import * as errors from '../errors';
|
||||
import { localize } from '../localize';
|
||||
|
||||
export async function showQuickPick<T>(items: PickWithData<T>[] | Thenable<PickWithData<T>[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<PickWithData<T>>;
|
||||
export async function showQuickPick(items: Pick[] | Thenable<Pick[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<Pick>;
|
||||
export async function showQuickPick(items: vscode.QuickPickItem[] | Thenable<vscode.QuickPickItem[]>, placeHolder: string, ignoreFocusOut: boolean = false): Promise<vscode.QuickPickItem> {
|
||||
const options: vscode.QuickPickOptions = {
|
||||
placeHolder: placeHolder,
|
||||
ignoreFocusOut: ignoreFocusOut
|
||||
};
|
||||
const result: vscode.QuickPickItem | undefined = await vscode.window.showQuickPick(items, options);
|
||||
|
||||
if (!result) {
|
||||
throw new errors.UserCancelledError();
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function showInputBox(placeHolder: string, prompt: string, ignoreFocusOut: boolean = false, validateInput?: (s: string) => string | undefined | null): Promise<string> {
|
||||
const options: vscode.InputBoxOptions = {
|
||||
placeHolder: placeHolder,
|
||||
prompt: prompt,
|
||||
validateInput: validateInput,
|
||||
ignoreFocusOut: ignoreFocusOut
|
||||
};
|
||||
const result: string | undefined = await vscode.window.showInputBox(options);
|
||||
|
||||
if (!result) {
|
||||
throw new errors.UserCancelledError();
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function showFolderDialog(): Promise<string> {
|
||||
const defaultUri: vscode.Uri | undefined = vscode.workspace.rootPath ? vscode.Uri.file(vscode.workspace.rootPath) : undefined;
|
||||
const options: vscode.OpenDialogOptions = {
|
||||
defaultUri: defaultUri,
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
openLabel: localize('azFunc.select', 'Select')
|
||||
};
|
||||
const result: vscode.Uri[] | undefined = await vscode.window.showOpenDialog(options);
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
throw new errors.UserCancelledError();
|
||||
} else {
|
||||
return result[0].fsPath;
|
||||
}
|
||||
}
|
||||
|
||||
export class Pick implements QuickPickItem {
|
||||
public readonly description: string;
|
||||
public readonly label: string;
|
||||
constructor(label: string, description?: string) {
|
||||
this.label = label;
|
||||
this.description = description ? description : '';
|
||||
}
|
||||
}
|
||||
|
||||
export class PickWithData<T> extends Pick {
|
||||
public readonly data: T;
|
||||
constructor(data: T, label: string, description?: string) {
|
||||
super(label, description);
|
||||
this.data = data;
|
||||
}
|
||||
}
|
|
@ -5,20 +5,20 @@
|
|||
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { IUserInterface, PickWithData } from '../IUserInterface';
|
||||
import { localize } from '../localize';
|
||||
import * as uiUtil from './ui';
|
||||
|
||||
export async function selectWorkspaceFolder(placeholder: string): Promise<string> {
|
||||
export async function selectWorkspaceFolder(ui: IUserInterface, placeholder: string): Promise<string> {
|
||||
const browse: string = ':browse';
|
||||
let folder: uiUtil.PickWithData<string> | undefined;
|
||||
let folder: PickWithData<string> | undefined;
|
||||
if (vscode.workspace.workspaceFolders) {
|
||||
let folderPicks: uiUtil.PickWithData<string>[] = [new uiUtil.PickWithData(browse, localize('azFunc.browse', '$(file-directory) Browse...'))];
|
||||
folderPicks = folderPicks.concat(vscode.workspace.workspaceFolders.map((f: vscode.WorkspaceFolder) => new uiUtil.PickWithData('', f.uri.fsPath)));
|
||||
let folderPicks: PickWithData<string>[] = [new PickWithData(browse, localize('azFunc.browse', '$(file-directory) Browse...'))];
|
||||
folderPicks = folderPicks.concat(vscode.workspace.workspaceFolders.map((f: vscode.WorkspaceFolder) => new PickWithData('', f.uri.fsPath)));
|
||||
|
||||
folder = await uiUtil.showQuickPick<string>(folderPicks, placeholder);
|
||||
folder = await ui.showQuickPick<string>(folderPicks, placeholder);
|
||||
}
|
||||
|
||||
return folder && folder.data !== browse ? folder.label : await uiUtil.showFolderDialog();
|
||||
return folder && folder.data !== browse ? folder.label : await ui.showFolderDialog();
|
||||
}
|
||||
|
||||
export function isFolderOpenInWorkspace(fsPath: string): boolean {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vscode';
|
||||
import { AzureAccount, AzureLoginStatus, AzureResourceFilter, AzureSession } from '../src/azure-account.api';
|
||||
|
||||
export class TestAzureAccount implements AzureAccount {
|
||||
public readonly status: AzureLoginStatus;
|
||||
public readonly onStatusChanged: Event<AzureLoginStatus>;
|
||||
public readonly sessions: AzureSession[] = [];
|
||||
public readonly onSessionsChanged: Event<void>;
|
||||
public readonly filters: AzureResourceFilter[] = [];
|
||||
public readonly onFiltersChanged: Event<void>;
|
||||
public async waitForLogin(): Promise<boolean> { return false; }
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { IUserInterface, Pick, PickWithData } from '../src/IUserInterface';
|
||||
|
||||
export class TestUI implements IUserInterface {
|
||||
private _inputs: (string | undefined)[];
|
||||
|
||||
constructor(inputs: (string | undefined)[]) {
|
||||
this._inputs = inputs;
|
||||
}
|
||||
|
||||
public async showQuickPick<T>(items: PickWithData<T>[] | Thenable<PickWithData<T>[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<PickWithData<T>>;
|
||||
public async showQuickPick(items: Pick[] | Thenable<Pick[]>, placeHolder: string, ignoreFocusOut?: boolean): Promise<Pick>;
|
||||
public async showQuickPick(items: vscode.QuickPickItem[] | Thenable<vscode.QuickPickItem[]>, placeHolder: string, _ignoreFocusOut: boolean = false): Promise<vscode.QuickPickItem> {
|
||||
if (this._inputs.length > 0) {
|
||||
const input: string | undefined = this._inputs.shift();
|
||||
const resolvedItems: vscode.QuickPickItem[] = await Promise.resolve(items);
|
||||
|
||||
if (resolvedItems.length === 0) {
|
||||
throw new Error(`No quick pick items found. Placeholder: '${placeHolder}'`);
|
||||
} else if (input) {
|
||||
const resolvedItem: vscode.QuickPickItem | undefined = resolvedItems.find((qpi: vscode.QuickPickItem) => qpi.label === input);
|
||||
if (resolvedItem) {
|
||||
return resolvedItem;
|
||||
} else {
|
||||
throw new Error(`Did not find quick pick item matching '${input}'. Placeholder: '${placeHolder}'`);
|
||||
}
|
||||
} else {
|
||||
// Use default value if input is undefined
|
||||
return resolvedItems[0];
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected call to showQuickPick. Placeholder: '${placeHolder}'`);
|
||||
}
|
||||
|
||||
public async showInputBox(placeHolder: string, prompt: string, _ignoreFocusOut: boolean = false, validateInput?: (s: string) => string | undefined | null, value?: string): Promise<string> {
|
||||
if (this._inputs.length > 0) {
|
||||
let result: string | undefined = this._inputs.shift();
|
||||
if (!result) {
|
||||
// Use default value if input is undefined
|
||||
result = value;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
if (validateInput) {
|
||||
const msg: string | null | undefined = validateInput(result);
|
||||
if (msg !== null && msg !== undefined) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected call to showInputBox. Placeholder: '${placeHolder}'. Prompt: '${prompt}'`);
|
||||
}
|
||||
|
||||
public async showFolderDialog(): Promise<string> {
|
||||
if (this._inputs.length > 0) {
|
||||
const result: string | undefined = this._inputs.shift();
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unexpected call to showFolderDialog');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { createFunction } from '../src/commands/createFunction';
|
||||
import { TemplateData } from '../src/templates/TemplateData';
|
||||
import * as fsUtil from '../src/utils/fs';
|
||||
import { TestAzureAccount } from './TestAzureAccount';
|
||||
import { TestUI } from './TestUI';
|
||||
|
||||
const templateData: TemplateData = new TemplateData();
|
||||
const testFolder: string = path.join(os.tmpdir(), `azFunc.createFuncTests${fsUtil.randomName()}`);
|
||||
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel('Azure Functions Test');
|
||||
|
||||
suiteSetup(async () => {
|
||||
await fsUtil.makeFolder(testFolder);
|
||||
await fsUtil.makeFolder(path.join(testFolder, '.vscode'));
|
||||
// Pretend to create the parent function app
|
||||
await Promise.all([
|
||||
fsUtil.writeToFile(path.join(testFolder, 'host.json'), ''),
|
||||
fsUtil.writeToFile(path.join(testFolder, 'local.settings.json'), ''),
|
||||
fsUtil.writeToFile(path.join(testFolder, '.vscode', 'launch.json'), '')
|
||||
]);
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
outputChannel.dispose();
|
||||
await fsUtil.deleteFolderAndContents(testFolder);
|
||||
});
|
||||
|
||||
suite('Create Function Tests', () => {
|
||||
const blobTrigger: string = 'BlobTrigger';
|
||||
test(blobTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
blobTrigger,
|
||||
'storageAccountConnection',
|
||||
undefined // Use default path
|
||||
);
|
||||
});
|
||||
|
||||
const cosmosDBTrigger: string = 'CosmosDBTrigger';
|
||||
test(cosmosDBTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
cosmosDBTrigger,
|
||||
'cosmosDBConnection',
|
||||
'dbName',
|
||||
'collectionName',
|
||||
undefined, // Use default for 'create leases if doesn't exist'
|
||||
undefined // Use default lease name
|
||||
);
|
||||
});
|
||||
|
||||
const eventHubTrigger: string = 'EventHubTrigger';
|
||||
test(eventHubTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
eventHubTrigger,
|
||||
'eventHubConnection',
|
||||
undefined, // Use default event hub name
|
||||
undefined // Use default event hub consumer group
|
||||
);
|
||||
});
|
||||
|
||||
const genericWebhook: string = 'Generic Webhook';
|
||||
test(genericWebhook, async () => {
|
||||
await testCreateFunction(genericWebhook);
|
||||
});
|
||||
|
||||
const gitHubWebhook: string = 'GitHub Webhook';
|
||||
test(gitHubWebhook, async () => {
|
||||
await testCreateFunction(gitHubWebhook);
|
||||
});
|
||||
|
||||
const httpTrigger: string = 'HttpTrigger';
|
||||
test(httpTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
httpTrigger,
|
||||
undefined // Use default Authorization level
|
||||
);
|
||||
});
|
||||
|
||||
const httpTriggerWithParameters: string = 'HttpTriggerWithParameters';
|
||||
test(httpTriggerWithParameters, async () => {
|
||||
await testCreateFunction(
|
||||
httpTriggerWithParameters,
|
||||
undefined // Use default Authorization level
|
||||
);
|
||||
});
|
||||
|
||||
const manualTrigger: string = 'ManualTrigger';
|
||||
test(manualTrigger, async () => {
|
||||
await testCreateFunction(manualTrigger);
|
||||
});
|
||||
|
||||
const queueTrigger: string = 'QueueTrigger';
|
||||
test(queueTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
queueTrigger,
|
||||
'storageAccountConnection',
|
||||
undefined // Use default queue name
|
||||
);
|
||||
});
|
||||
|
||||
const serviceBusQueueTrigger: string = 'ServiceBusQueueTrigger';
|
||||
test(serviceBusQueueTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
serviceBusQueueTrigger,
|
||||
'serviceBusConnection',
|
||||
'accessRights',
|
||||
undefined // Use default queue name
|
||||
);
|
||||
});
|
||||
|
||||
const serviceBusTopicTrigger: string = 'ServiceBusTopicTrigger';
|
||||
test(serviceBusTopicTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
serviceBusTopicTrigger,
|
||||
'serviceBusConnection',
|
||||
'accessRights',
|
||||
undefined, // Use default topic name
|
||||
undefined // Use default subscription name
|
||||
);
|
||||
});
|
||||
|
||||
const timerTrigger: string = 'TimerTrigger';
|
||||
test(timerTrigger, async () => {
|
||||
await testCreateFunction(
|
||||
timerTrigger,
|
||||
undefined // Use default schedule
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function testCreateFunction(funcName: string, ...inputs: (string | undefined)[]): Promise<void> {
|
||||
// Setup common inputs
|
||||
inputs.unshift(funcName); // Specify the function name
|
||||
inputs.unshift(funcName); // Select the function template
|
||||
inputs.unshift(testFolder); // Select the test func app folder
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
|
||||
inputs.unshift(undefined); // If the test environment has an open workspace, select the 'Browse...' option
|
||||
}
|
||||
|
||||
const ui: TestUI = new TestUI(inputs);
|
||||
await createFunction(outputChannel, new TestAzureAccount(), templateData, ui);
|
||||
assert.equal(inputs.length, 0, 'Not all inputs were used.');
|
||||
|
||||
const functionPath: string = path.join(testFolder, funcName);
|
||||
assert.equal(await fsUtil.fsPathExists(path.join(functionPath, 'index.js')), true, 'index.js does not exist');
|
||||
assert.equal(await fsUtil.fsPathExists(path.join(functionPath, 'function.json')), true, 'function.json does not exist');
|
||||
}
|
|
@ -6,6 +6,8 @@ open source projects along with the license information below. We acknowledge an
|
|||
are grateful to these developers for their contribution to open source.
|
||||
|
||||
1. opn (https://github.com/sindresorhus/opn)
|
||||
1. rimraf (https://github.com/isaacs/rimraf)
|
||||
1. request-promise (https://github.com/request/request-promise)
|
||||
|
||||
opn NOTICES BEGIN HERE
|
||||
=============================
|
||||
|
@ -34,3 +36,47 @@ THE SOFTWARE.
|
|||
|
||||
END OF opn NOTICES AND INFORMATION
|
||||
==================================
|
||||
|
||||
rimraf NOTICES BEGIN HERE
|
||||
=============================
|
||||
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
END OF rimraf NOTICES AND INFORMATION
|
||||
==================================
|
||||
|
||||
request-promise NOTICES BEGIN HERE
|
||||
=============================
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2017, Nicolai Kamenzky, Ty Abonil, and contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
END OF request-promise NOTICES AND INFORMATION
|
||||
==================================
|
|
@ -196,7 +196,12 @@
|
|||
],
|
||||
"underscore-consistent-invocation": true,
|
||||
"unified-signatures": true,
|
||||
"variable-name": true,
|
||||
"variable-name": [ // changed
|
||||
true,
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-leading-underscore"
|
||||
],
|
||||
"react-a11y-anchors": true,
|
||||
"react-a11y-aria-unsupported-elements": true,
|
||||
"react-a11y-event-has-role": true,
|
||||
|
|
Загрузка…
Ссылка в новой задаче