apps/vs-code-designer/src/app/tree/LogicAppResourceTree.ts (337 lines of code) (raw):
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { ContainerApp } from '@azure/arm-appcontainers';
import { localSettingsFileName } from '../../constants';
import { localize } from '../../localize';
import { parseHostJson } from '../funcConfig/host';
import { getLocalSettingsJson } from '../utils/appSettings/localSettings';
import { getFileOrFolderContent } from '../utils/codeless/apiUtils';
import { tryParseFuncVersion } from '../utils/funcCoreTools/funcVersion';
import { getIconPath } from '../utils/tree/assets';
import { matchesAnyPart } from '../utils/tree/projectContextValues';
import { ConfigurationsTreeItem } from './configurationsTree/ConfigurationsTreeItem';
import { RemoteWorkflowsTreeItem } from './remoteWorkflowsTree/RemoteWorkflowsTreeItem';
import type { SlotTreeItem } from './slotsTree/SlotTreeItem';
import { SlotsTreeItem } from './slotsTree/SlotsTreeItem';
import { ArtifactsTreeItem } from './slotsTree/artifactsTree/ArtifactsTreeItem';
import type { Site, SiteConfig, SiteSourceControl, StringDictionary } from '@azure/arm-appservice';
import { isString } from '@microsoft/logic-apps-shared';
import {
DeleteLastServicePlanStep,
DeleteSiteStep,
DeploymentsTreeItem,
DeploymentTreeItem,
getFile,
ParsedSite,
AppSettingsTreeItem,
LogFilesTreeItem,
SiteFilesTreeItem,
} from '@microsoft/vscode-azext-azureappservice';
import type { IDeployContext } from '@microsoft/vscode-azext-azureappservice';
import { AzureWizard, DeleteConfirmationStep, nonNullValue } from '@microsoft/vscode-azext-utils';
import type { AzExtTreeItem, IActionContext, ISubscriptionContext, TreeItemIconPath } from '@microsoft/vscode-azext-utils';
import type { ResolvedAppResourceBase } from '@microsoft/vscode-azext-utils/hostapi';
import { ProjectResource, ProjectSource, latestGAVersion } from '@microsoft/vscode-extension-logic-apps';
import type {
ApplicationSettings,
FuncHostRequest,
FuncVersion,
ILocalSettingsJson,
IParsedHostJson,
} from '@microsoft/vscode-extension-logic-apps';
import * as path from 'path';
export function isLogicAppResourceTree(ti: unknown): ti is ResolvedAppResourceBase {
return (ti as unknown as LogicAppResourceTree).instance === LogicAppResourceTree.instance;
}
export class LogicAppResourceTree implements ResolvedAppResourceBase {
public static instance = 'logicAppResourceTree';
public readonly instance = LogicAppResourceTree.instance;
public hybridSite: ContainerApp;
public site: ParsedSite;
public data: Site;
private _subscription: ISubscriptionContext;
public logStreamPath = '';
public appSettingsTreeItem: AppSettingsTreeItem;
public deploymentsNode: DeploymentsTreeItem | undefined;
public readonly source: ProjectSource = ProjectSource.Remote;
public contextValuesToAdd?: string[] | undefined;
public maskedValuesToAdd: string[] = [];
public configurationsTreeItem: ConfigurationsTreeItem;
private _cachedVersion: FuncVersion | undefined;
private _cachedHostJson: IParsedHostJson | undefined;
private _workflowsTreeItem: RemoteWorkflowsTreeItem | undefined;
private _artifactsTreeItem: ArtifactsTreeItem;
private _logFilesTreeItem: LogFilesTreeItem;
private _siteFilesTreeItem: SiteFilesTreeItem;
private _slotsTreeItem: SlotsTreeItem;
private _cachedIsConsumption: boolean | undefined;
public static pickSlotContextValue = new RegExp(/azLogicAppsSlot(?!s)/);
public static productionContextValue = 'azLogicAppsProductionSlot';
public static slotContextValue = 'azLogicAppsSlot';
commandId?: string | undefined;
tooltip?: string | undefined;
commandArgs?: unknown[] | undefined;
public constructor(subscription: ISubscriptionContext, site: Site) {
if (site.id.includes('Microsoft.Web')) {
this.site = new ParsedSite(site, subscription);
this.data = this.site.rawSite;
this._subscription = subscription;
this.contextValuesToAdd = [this.site.isSlot ? LogicAppResourceTree.slotContextValue : LogicAppResourceTree.productionContextValue];
const valuesToMask = [
this.site.siteName,
this.site.slotName,
this.site.defaultHostName,
this.site.resourceGroup,
this.site.planName,
this.site.planResourceGroup,
this.site.kuduHostName,
this.site.gitUrl,
this.site.rawSite.repositorySiteName,
...(this.site.rawSite.hostNames || []),
...(this.site.rawSite.enabledHostNames || []),
];
for (const v of valuesToMask) {
if (v) {
this.maskedValuesToAdd.push(v);
}
}
} else {
this.hybridSite = site as unknown as ContainerApp;
}
}
public static createLogicAppResourceTree(context: IActionContext, subscription: ISubscriptionContext, site: Site): LogicAppResourceTree {
const resource = new LogicAppResourceTree(subscription, site);
resource.site.createClient(context).then(async (client) => (resource.data.siteConfig = await client.getSiteConfig()));
return resource;
}
public get name(): string {
return this.label;
}
public get label(): string {
return this.site.slotName ?? this.site.fullName;
}
public get id(): string {
return this.site.id;
}
public get logStreamLabel(): string {
return this.site.fullName;
}
public async getHostRequest(): Promise<FuncHostRequest> {
return { url: this.site.defaultHostUrl };
}
public get description(): string | undefined {
return this._state?.toLowerCase() !== 'running' ? this._state : undefined;
}
public get iconPath(): TreeItemIconPath {
const proxyTree: SlotTreeItem = this as unknown as SlotTreeItem;
return getIconPath(proxyTree.contextValue);
}
private get _state(): string | undefined {
return this.site.rawSite.state;
}
public hasMoreChildrenImpl(): boolean {
return false;
}
/**
* NOTE: We need to be extra careful in this method because it blocks many core scenarios (e.g. deploy) if the tree item is listed as invalid
*/
public async refreshImpl(context: IActionContext): Promise<void> {
this._cachedVersion = undefined;
this._cachedHostJson = undefined;
this._cachedIsConsumption = undefined;
const client = await this.site.createClient(context);
this.site = new ParsedSite(nonNullValue(await client.getSite(), 'site'), this._subscription);
}
public async getVersion(context: IActionContext): Promise<FuncVersion> {
let result: FuncVersion | undefined = this._cachedVersion;
if (result === undefined) {
let version: FuncVersion | undefined;
try {
const client = await this.site.createClient(context);
const appSettings: StringDictionary = await client.listApplicationSettings();
version = tryParseFuncVersion(appSettings.properties && appSettings.properties.FUNCTIONS_EXTENSION_VERSION);
} catch {
// ignore and use default
}
// tslint:disable-next-line: strict-boolean-expressions
result = version || latestGAVersion;
this._cachedVersion = result;
}
return result;
}
public async getHostJson(context: IActionContext): Promise<IParsedHostJson> {
let result: IParsedHostJson | undefined = this._cachedHostJson;
if (!result) {
let data: any;
try {
data = JSON.parse((await getFile(context, this.site, 'site/wwwroot/host.json')).data);
} catch {
// ignore and use default
}
const version: FuncVersion = await this.getVersion(context);
result = parseHostJson(data, version);
this._cachedHostJson = result;
}
return result;
}
public async getApplicationSettings(context: IDeployContext): Promise<ApplicationSettings> {
const localSettings: ILocalSettingsJson = await getLocalSettingsJson(
context,
path.join(context.effectiveDeployFsPath, localSettingsFileName)
);
return localSettings.Values || {};
}
public async setApplicationSetting(context: IActionContext, key: string, value: string): Promise<void> {
const client = await this.site.createClient(context);
const settings: StringDictionary = await client.listApplicationSettings();
if (!settings.properties) {
settings.properties = {};
}
settings.properties[key] = value;
await client.updateApplicationSettings(settings);
}
public async getIsConsumption(context: IActionContext): Promise<boolean> {
let result: boolean | undefined = this._cachedIsConsumption;
if (result === undefined) {
try {
const client = await this.site.createClient(context);
result = await client.getIsConsumption(context);
} catch {
// ignore and use default
result = true;
}
this._cachedIsConsumption = result;
}
return result;
}
public async loadMoreChildrenImpl(_clearCache: boolean, context: IActionContext): Promise<AzExtTreeItem[]> {
const client = await this.site.createClient(context);
const siteConfig: SiteConfig = await client.getSiteConfig();
const sourceControl: SiteSourceControl = await client.getSourceControl();
const proxyTree: SlotTreeItem = this as unknown as SlotTreeItem;
this.deploymentsNode = new DeploymentsTreeItem(proxyTree, {
site: this.site,
siteConfig: siteConfig,
sourceControl: sourceControl,
});
this.deploymentsNode = new DeploymentsTreeItem(proxyTree, {
site: this.site,
siteConfig,
sourceControl,
contextValuesToAdd: ['azLogicApps'],
});
this.appSettingsTreeItem = new AppSettingsTreeItem(proxyTree, this.site, {
contextValuesToAdd: ['azLogicApps'],
});
this._siteFilesTreeItem = new SiteFilesTreeItem(proxyTree, {
site: this.site,
isReadOnly: true,
contextValuesToAdd: ['azLogicApps'],
});
this._logFilesTreeItem = new LogFilesTreeItem(proxyTree, {
site: this.site,
contextValuesToAdd: ['azLogicApps'],
});
if (!this._workflowsTreeItem) {
this._workflowsTreeItem = await RemoteWorkflowsTreeItem.createWorkflowsTreeItem(context, proxyTree);
}
if (!this.configurationsTreeItem) {
this.configurationsTreeItem = await ConfigurationsTreeItem.createConfigurationsTreeItem(proxyTree, context);
}
const children: AzExtTreeItem[] = [
this._workflowsTreeItem,
this.configurationsTreeItem,
this._siteFilesTreeItem,
this._logFilesTreeItem,
this.deploymentsNode,
];
if (!this.site.isSlot) {
this._slotsTreeItem = new SlotsTreeItem(proxyTree);
children.push(this._slotsTreeItem);
}
if (!this._artifactsTreeItem) {
try {
await getFileOrFolderContent(context, proxyTree, 'Artifacts');
} catch (error) {
if (error.statusCode === 404) {
return children;
}
}
this._artifactsTreeItem = new ArtifactsTreeItem(proxyTree, this.site);
children.push(this._artifactsTreeItem);
}
return children;
}
public async pickTreeItemImpl(expectedContextValues: (string | RegExp)[]): Promise<AzExtTreeItem | undefined> {
if (!this.site.isSlot) {
for (const expectedContextValue of expectedContextValues) {
switch (expectedContextValue) {
case SlotsTreeItem.contextValue:
case LogicAppResourceTree.slotContextValue:
return this._slotsTreeItem;
default:
}
}
}
for (const expectedContextValue of expectedContextValues) {
if (expectedContextValue instanceof RegExp) {
const appSettingsContextValues = [ConfigurationsTreeItem.contextValue];
if (matchContextValue(expectedContextValue, appSettingsContextValues)) {
return this.configurationsTreeItem;
}
const deploymentsContextValues = [
DeploymentsTreeItem.contextValueConnected,
DeploymentsTreeItem.contextValueUnconnected,
DeploymentTreeItem.contextValue,
];
if (matchContextValue(expectedContextValue, deploymentsContextValues)) {
return this.deploymentsNode;
}
if (matchContextValue(expectedContextValue, [LogicAppResourceTree.slotContextValue])) {
return this._slotsTreeItem;
}
}
if (isString(expectedContextValue)) {
// DeploymentTreeItem.contextValue is a RegExp, but the passed in contextValue can be a string so check for a match
if (DeploymentTreeItem.contextValue.test(expectedContextValue)) {
return this.deploymentsNode;
}
} else if (matchesAnyPart(expectedContextValue, ProjectResource.Workflows, ProjectResource.Workflow)) {
return this._workflowsTreeItem;
}
}
return undefined;
}
public compareChildrenImpl(): number {
return 0; // already sorted
}
public async isReadOnly(context: IActionContext): Promise<boolean> {
const client = await this.site.createClient(context);
const appSettings: StringDictionary = await client.listApplicationSettings();
return !!appSettings.properties && !!(appSettings.properties.WEBSITE_RUN_FROM_PACKAGE || appSettings.properties.WEBSITE_RUN_FROM_ZIP);
}
public async deleteTreeItemImpl(context: IActionContext): Promise<void> {
const { isSlot, fullName, isFunctionApp } = this.site;
const confirmationMessage: string = isSlot
? localize('confirmDeleteSlot', 'Are you sure you want to delete slot "{0}"?', fullName)
: isFunctionApp
? localize('confirmDeleteFunctionApp', 'Are you sure you want to delete function app "{0}"?', fullName)
: localize('confirmDeleteWebApp', 'Are you sure you want to delete web app "{0}"?', fullName);
const wizardContext = Object.assign(context, {
site: this.site,
});
const wizard = new AzureWizard(wizardContext, {
title: localize('deleteSwa', 'Delete Function App "{0}"', this.label),
promptSteps: [new DeleteConfirmationStep(confirmationMessage), new DeleteLastServicePlanStep()],
executeSteps: [new DeleteSiteStep()],
});
await wizard.prompt();
await wizard.execute();
}
}
function matchContextValue(expectedContextValue: RegExp | string, matches: (string | RegExp)[]): boolean {
if (expectedContextValue instanceof RegExp) {
return matches.some((match) => {
if (match instanceof RegExp) {
return expectedContextValue.toString() === match.toString();
}
return expectedContextValue.test(match);
});
}
return matches.some((match) => {
if (match instanceof RegExp) {
return match.test(expectedContextValue);
}
return expectedContextValue === match;
});
}