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:
Родитель
2b5d429360
Коммит
9ee1fa38bf
|
@ -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));
|
||||
|
|
Загрузка…
Ссылка в новой задаче