Refactor common node functionality

- Move repeated code (like getIconPath) into NodeBase
- Implement showNodePicker so that commands work from the command pallete
- Move start/stop/restart logic into command files and show function app state in explorer instead of output window
This commit is contained in:
Eric Jizba 2017-10-02 09:08:37 -07:00
Родитель 2b5d429360
Коммит 9ee1fa38bf
12 изменённых файлов: 163 добавлений и 127 удалений

Просмотреть файл

@ -156,7 +156,7 @@
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"lint": "tslint --project tsconfig.json src/*.ts -e src/*.d.ts --type-check -t verbose",
"lint": "tslint --project tsconfig.json -e src/*.d.ts --type-check -t verbose",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
},
@ -180,4 +180,4 @@
"extensionDependencies": [
"ms-vscode.azure-account"
]
}
}

Просмотреть файл

@ -5,13 +5,14 @@
import { Event, EventEmitter, TreeDataProvider, TreeItem } from 'vscode';
import { AzureAccount, AzureResourceFilter } from './azure-account.api';
import { GenericNode } from './nodes/GenericNode';
import { NodeBase } from './nodes/NodeBase';
import { SubscriptionNode } from './nodes/SubscriptionNode';
import * as util from './util';
export class AzureFunctionsExplorer implements TreeDataProvider<NodeBase> {
private onDidChangeTreeDataEmitter: EventEmitter<NodeBase> = new EventEmitter<NodeBase>();
private azureAccount: AzureAccount;
private rootNodes: NodeBase[] = [];
constructor(azureAccount: AzureAccount) {
this.azureAccount = azureAccount;
@ -27,16 +28,20 @@ export class AzureFunctionsExplorer implements TreeDataProvider<NodeBase> {
public async getChildren(node?: NodeBase): Promise<NodeBase[]> {
if (node) {
return node.getChildren ? await node.getChildren() : [];
return await node.getChildren();
} else { // Root of the explorer
this.rootNodes = [];
if (this.azureAccount.status === 'Initializing' || this.azureAccount.status === 'LoggingIn') {
return [new GenericNode('azureFunctionsLoading', 'Loading...')];
return [new NodeBase('azureFunctionsLoading', 'Loading...')];
} else if (this.azureAccount.status === 'LoggedOut') {
return [new GenericNode('azureFunctionsSignInToAzure', 'Sign in to Azure...', 'azure-account.login')];
return [new NodeBase('azureFunctionsSignInToAzure', 'Sign in to Azure...', undefined, 'azure-account.login')];
} else if (this.azureAccount.filters.length === 0) {
return [new GenericNode('azureFunctionsNoSubscriptions', 'No subscriptions found. Edit filters...', 'azure-account.selectSubscriptions')];
return [new NodeBase('azureFunctionsNoSubscriptions', 'No subscriptions found. Edit filters...', undefined, 'azure-account.selectSubscriptions')];
} else {
return this.azureAccount.filters.map((filter: AzureResourceFilter) => new SubscriptionNode(filter));
this.rootNodes = this.azureAccount.filters.map((filter: AzureResourceFilter) => SubscriptionNode.CREATE(filter));
return this.rootNodes;
}
}
}
@ -44,4 +49,24 @@ export class AzureFunctionsExplorer implements TreeDataProvider<NodeBase> {
public refresh(node?: NodeBase): void {
this.onDidChangeTreeDataEmitter.fire(node);
}
public async showNodePicker(expectedContextValue: string): Promise<NodeBase> {
let childType: string | undefined = 'Subscription';
let quickPicksTask: Promise<util.PickWithData<NodeBase>[]> = Promise.resolve(this.rootNodes.map((c: NodeBase) => new util.PickWithData<NodeBase>(c, c.label)));
while (childType) {
const pick: util.PickWithData<NodeBase> = await util.showQuickPick<NodeBase>(quickPicksTask, `Select a ${childType}`);
const node: NodeBase = pick.data;
if (node.contextValue === expectedContextValue) {
return node;
}
childType = node.childType;
quickPicksTask = node.getChildren(false).then((nodes: NodeBase[]): util.PickWithData<NodeBase>[] => {
return nodes.map((c: NodeBase) => new util.PickWithData<NodeBase>(c, c.label));
});
}
throw new Error('No matching resources found.');
}
}

Просмотреть файл

@ -7,7 +7,7 @@ import * as opn from 'opn';
import { NodeBase } from '../nodes/NodeBase';
export function openInPortal(node?: NodeBase): void {
if (node && node.tenantId) {
if (node) {
(<(s: string) => void>opn)(`https://portal.azure.com/${node.tenantId}/#resource${node.id}`);
}
}

Просмотреть файл

@ -3,13 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { AzureFunctionsExplorer } from '../AzureFunctionsExplorer';
import { FunctionAppNode } from '../nodes/FunctionAppNode';
import { startFunctionApp } from './startFunctionApp';
import { stopFunctionApp } from './stopFunctionApp';
export async function restartFunctionApp(outputChannel: vscode.OutputChannel, node?: FunctionAppNode): Promise<void> {
if (node) {
outputChannel.appendLine(`Restarting Function App "${node.label}"...`);
await node.restart();
outputChannel.appendLine(`Function App "${node.label}" has been restarted.`);
}
export async function restartFunctionApp(explorer: AzureFunctionsExplorer, node?: FunctionAppNode): Promise<void> {
await startFunctionApp(explorer, node);
await stopFunctionApp(explorer, node);
}

Просмотреть файл

@ -3,13 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
// tslint:disable-next-line:no-require-imports
import WebSiteManagementClient = require('azure-arm-website');
import { AzureFunctionsExplorer } from '../AzureFunctionsExplorer';
import { FunctionAppNode } from '../nodes/FunctionAppNode';
import * as util from '../util';
export async function startFunctionApp(outputChannel: vscode.OutputChannel, node?: FunctionAppNode): Promise<void> {
if (node) {
outputChannel.appendLine(`Starting Function App "${node.label}"...`);
await node.start();
outputChannel.appendLine(`Function App "${node.label}" has been started.`);
export async function startFunctionApp(explorer: AzureFunctionsExplorer, node?: FunctionAppNode): Promise<void> {
if (!node) {
node = <FunctionAppNode>(await explorer.showNodePicker(FunctionAppNode.contextValue));
}
const client: WebSiteManagementClient = node.getWebSiteClient();
await client.webApps.start(node.resourceGroup, node.name);
await util.waitForFunctionAppState(client, node.resourceGroup, node.name, util.FunctionAppState.Running);
explorer.refresh(node.parent);
}

Просмотреть файл

@ -3,13 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
// tslint:disable-next-line:no-require-imports
import WebSiteManagementClient = require('azure-arm-website');
import { AzureFunctionsExplorer } from '../AzureFunctionsExplorer';
import { FunctionAppNode } from '../nodes/FunctionAppNode';
import * as util from '../util';
export async function stopFunctionApp(outputChannel: vscode.OutputChannel, node?: FunctionAppNode): Promise<void> {
if (node) {
outputChannel.appendLine(`Stopping Function App "${node.label}"...`);
await node.stop();
outputChannel.appendLine(`Function App "${node.label}" has been stopped.`);
export async function stopFunctionApp(explorer: AzureFunctionsExplorer, node?: FunctionAppNode): Promise<void> {
if (!node) {
node = <FunctionAppNode>(await explorer.showNodePicker(FunctionAppNode.contextValue));
}
const client: WebSiteManagementClient = node.getWebSiteClient();
await client.webApps.stop(node.resourceGroup, node.name);
await util.waitForFunctionAppState(client, node.resourceGroup, node.name, util.FunctionAppState.Stopped);
explorer.refresh(node.parent);
}

Просмотреть файл

@ -49,9 +49,9 @@ export function activate(context: vscode.ExtensionContext): void {
initCommand(context, 'azureFunctions.openInPortal', openInPortal);
initAsyncCommand(context, 'azureFunctions.createFunction', async () => await createFunction(outputChannel));
initAsyncCommand(context, 'azureFunctions.createFunctionApp', async () => await createFunctionApp(outputChannel));
initAsyncCommand(context, 'azureFunctions.startFunctionApp', async (node?: FunctionAppNode) => await startFunctionApp(outputChannel, node));
initAsyncCommand(context, 'azureFunctions.stopFunctionApp', async (node?: FunctionAppNode) => await stopFunctionApp(outputChannel, node));
initAsyncCommand(context, 'azureFunctions.restartFunctionApp', async (node?: FunctionAppNode) => await restartFunctionApp(outputChannel, node));
initAsyncCommand(context, 'azureFunctions.startFunctionApp', async (node?: FunctionAppNode) => await startFunctionApp(explorer, node));
initAsyncCommand(context, 'azureFunctions.stopFunctionApp', async (node?: FunctionAppNode) => await stopFunctionApp(explorer, node));
initAsyncCommand(context, 'azureFunctions.restartFunctionApp', async (node?: FunctionAppNode) => await restartFunctionApp(explorer, node));
}
}

Просмотреть файл

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// tslint:disable-next-line:import-name no-require-imports
// tslint:disable-next-line:no-require-imports
import WebSiteManagementClient = require('azure-arm-website');
import * as path from 'path';
import * as vscode from 'vscode';
@ -13,50 +13,23 @@ import * as util from '../util';
import { NodeBase } from './NodeBase';
import { SubscriptionNode } from './SubscriptionNode';
export class FunctionAppNode implements NodeBase {
public readonly contextValue: string = 'azureFunctionsFunctionApp';
public readonly label: string;
public readonly id: string;
public readonly tenantId: string;
export class FunctionAppNode extends NodeBase {
public static readonly contextValue: string = 'azureFunctionsFunctionApp';
public readonly name: string;
public readonly resourceGroup: string;
public readonly parent: SubscriptionNode;
private readonly resourceGroup: string;
private readonly name: string;
private readonly subscriptionNode: SubscriptionNode;
private constructor(id: string, name: string, state: string, resourceGroup: string) {
super(id, state === util.FunctionAppState.Running ? name : `${name} (${state})`, FunctionAppNode.contextValue);
this.resourceGroup = resourceGroup;
this.name = name;
}
constructor(functionApp: Site, subscriptionNode: SubscriptionNode) {
if (!functionApp.id || !functionApp.resourceGroup || !functionApp.name) {
public static CREATE(functionApp: Site): FunctionAppNode {
if (!functionApp.id || !functionApp.name || !functionApp.state || !functionApp.resourceGroup) {
throw new errors.ArgumentError(functionApp);
}
this.id = functionApp.id;
this.resourceGroup = functionApp.resourceGroup;
this.name = functionApp.name;
this.subscriptionNode = subscriptionNode;
this.label = `${functionApp.name} (${this.resourceGroup})`;
this.tenantId = subscriptionNode.tenantId;
}
get iconPath(): { light: string, dark: string } {
return {
light: path.join(__filename, '..', '..', '..', '..', 'resources', 'light', `${this.contextValue}.svg`),
dark: path.join(__filename, '..', '..', '..', '..', 'resources', 'dark', `${this.contextValue}.svg`)
};
}
public async start(): Promise<void> {
const client: WebSiteManagementClient = this.subscriptionNode.getWebSiteClient();
await client.webApps.start(this.resourceGroup, this.name);
await util.waitForFunctionAppState(client, this.resourceGroup, this.name, util.FunctionAppState.Running);
}
public async stop(): Promise<void> {
const client: WebSiteManagementClient = this.subscriptionNode.getWebSiteClient();
await client.webApps.stop(this.resourceGroup, this.name);
await util.waitForFunctionAppState(client, this.resourceGroup, this.name, util.FunctionAppState.Stopped);
}
public async restart(): Promise<void> {
await this.stop();
await this.start();
return new FunctionAppNode(functionApp.id, functionApp.name, functionApp.state, functionApp.resourceGroup);
}
}

Просмотреть файл

@ -1,26 +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 { NodeBase } from './NodeBase';
export class GenericNode implements NodeBase {
public readonly contextValue: string;
public readonly command: vscode.Command;
public readonly id: string;
public readonly label: string;
constructor(id: string, label: string, commandId?: string) {
this.id = id;
this.label = label;
this.contextValue = id;
if (commandId) {
this.command = {
command: commandId,
title: ''
};
}
}
}

Просмотреть файл

@ -3,10 +3,68 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// tslint:disable-next-line:no-require-imports
import WebSiteManagementClient = require('azure-arm-website');
import * as path from 'path';
import * as vscode from 'vscode';
import { TreeItem } from 'vscode';
export interface NodeBase extends vscode.TreeItem {
id: string;
tenantId?: string;
getChildren?(): Promise<NodeBase[]>;
export class NodeBase implements TreeItem {
public static readonly contextValue: string = 'azureFunctionsNode';
public contextValue: string;
public label: string;
public id: string;
public collapsibleState: vscode.TreeItemCollapsibleState;
public parent: NodeBase;
public command?: vscode.Command;
public childType?: string;
private children: NodeBase[] | undefined;
constructor(id: string, label: string, contextValue?: string, commandId?: string) {
this.id = id;
this.label = label;
this.contextValue = contextValue || NodeBase.contextValue;
if (commandId) {
this.command = {
command: commandId,
title: ''
};
}
if (this.refreshChildren) {
this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
}
}
get iconPath(): { light: string, dark: string } | undefined {
if (this.contextValue !== NodeBase.contextValue) {
return {
light: path.join(__filename, '..', '..', '..', '..', 'resources', 'light', `${this.contextValue}.svg`),
dark: path.join(__filename, '..', '..', '..', '..', 'resources', 'dark', `${this.contextValue}.svg`)
};
}
}
public async getChildren(forceRefresh: boolean = true): Promise<NodeBase[]> {
if (this.refreshChildren && (!this.children || forceRefresh)) {
this.children = await this.refreshChildren();
this.children.forEach((node: NodeBase) => node.parent = this);
}
return this.children ? this.children : [];
}
get tenantId(): string {
// SubscriptionNode is the only node that needs to overwrite this
return this.parent.tenantId;
}
public getWebSiteClient(): WebSiteManagementClient {
// SubscriptionNode is the only node that needs to overwrite this
return this.parent.getWebSiteClient();
}
protected refreshChildren?(): Promise<NodeBase[]>;
}

Просмотреть файл

@ -13,39 +13,34 @@ import * as errors from '../errors';
import { FunctionAppNode } from './FunctionAppNode';
import { NodeBase } from './NodeBase';
export class SubscriptionNode implements NodeBase {
public readonly contextValue: string = 'azureFunctionsSubscription';
public readonly label: string;
public readonly id: string;
public readonly tenantId: string;
public readonly collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
export class SubscriptionNode extends NodeBase {
public static readonly contextValue: string = 'azureFunctionsSubscription';
public readonly childType: string = 'Function App';
private readonly subscriptionFilter: AzureResourceFilter;
constructor(subscriptionFilter: AzureResourceFilter) {
private constructor(id: string, name: string, subscriptionFilter: AzureResourceFilter) {
super(id, name, SubscriptionNode.contextValue);
this.subscriptionFilter = subscriptionFilter;
}
public static CREATE(subscriptionFilter: AzureResourceFilter): SubscriptionNode {
if (!subscriptionFilter.subscription.displayName || !subscriptionFilter.subscription.subscriptionId) {
throw new errors.ArgumentError(subscriptionFilter);
}
this.subscriptionFilter = subscriptionFilter;
this.label = subscriptionFilter.subscription.displayName;
this.id = subscriptionFilter.subscription.subscriptionId;
this.tenantId = subscriptionFilter.session.tenantId;
return new SubscriptionNode(subscriptionFilter.subscription.subscriptionId, subscriptionFilter.subscription.displayName, subscriptionFilter);
}
get iconPath(): { light: string, dark: string } {
return {
light: path.join(__filename, '..', '..', '..', '..', 'resources', 'light', `${this.contextValue}.svg`),
dark: path.join(__filename, '..', '..', '..', '..', 'resources', 'dark', `${this.contextValue}.svg`)
};
}
public async getChildren(): Promise<NodeBase[]> {
public async refreshChildren(): Promise<NodeBase[]> {
const webApps: WebAppCollection = await this.getWebSiteClient().webApps.list();
return webApps.filter((s: Site) => s.kind === 'functionapp')
.map((s: Site) => new FunctionAppNode(s, this))
.sort((f1: FunctionAppNode, f2: FunctionAppNode) => f1.id.localeCompare(f2.id));
.map((s: Site) => FunctionAppNode.CREATE(s));
}
get tenantId(): string {
return this.subscriptionFilter.session.tenantId;
}
public getWebSiteClient(): WebSiteManagementClient {

Просмотреть файл

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// tslint:disable-next-line:import-name no-require-imports
// tslint:disable-next-line:no-require-imports
import WebSiteManagementClient = require('azure-arm-website');
import * as fs from 'fs';
import * as vscode from 'vscode';
@ -77,8 +77,8 @@ export async function showFolderDialog(): Promise<string> {
}
export enum FunctionAppState {
Stopped = 'stopped',
Running = 'running'
Stopped = 'Stopped',
Running = 'Running'
}
export async function waitForFunctionAppState(webSiteManagementClient: WebSiteManagementClient, resourceGroup: string, name: string, state: FunctionAppState, intervalMs: number = 5000, timeoutMs: number = 60000): Promise<void> {
@ -86,7 +86,7 @@ export async function waitForFunctionAppState(webSiteManagementClient: WebSiteMa
while (count < timeoutMs) {
count += intervalMs;
const currentSite: Site = await webSiteManagementClient.webApps.get(resourceGroup, name);
if (currentSite.state && currentSite.state.toLowerCase() === state) {
if (currentSite.state && currentSite.state === state) {
return;
}
await new Promise((r: () => void): NodeJS.Timer => setTimeout(r, intervalMs));