Skip to content

Commit

Permalink
Feat(designer): Updates to Custom Code (boilerplate, host.json, optim…
Browse files Browse the repository at this point in the history
…izations) (#4690)

* fix(designer): fix issue where scope nodes didn't get focused when they were jumped to

* small pr

* added changes needed

---------

Co-authored-by: Travis Harris <travisharris@microsoft.com>
  • Loading branch information
Eric-B-Wu and hartra344 committed Apr 24, 2024
1 parent a6d85fe commit e911432
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 38 deletions.
Expand Up @@ -4,13 +4,14 @@ import { Artifact } from '../Models/Workflow';
import { validateResourceId } from '../Utilities/resourceUtilities';
import { convertDesignerWorkflowToConsumptionWorkflow } from './ConsumptionSerializationHelpers';
import type { AllCustomCodeFiles } from '@microsoft/logic-apps-designer';
import { CustomCodeService, LogEntryLevel, LoggerService, getAppFiles, mapFileExtensionToAppFileName } from '@microsoft/logic-apps-shared';
import { CustomCodeService, LogEntryLevel, LoggerService, getAppFileForFileExtension } from '@microsoft/logic-apps-shared';
import type { LogicAppsV2, VFSObject } from '@microsoft/logic-apps-shared';
import axios from 'axios';
import jwt_decode from 'jwt-decode';
import { useQuery } from 'react-query';
import { isSuccessResponse } from './HttpClient';
import { fetchFileData, fetchFilesFromFolder } from './vfsService';
import type { CustomCodeFileNameMapping } from '@microsoft/logic-apps-designer';

const baseUrl = 'https://management.azure.com';
const standardApiVersion = '2020-06-01';
Expand Down Expand Up @@ -47,10 +48,53 @@ export const useAllCustomCodeFiles = (appId?: string, workflowName?: string) =>
});
};

export const getCustomCodeAppFiles = async (appId?: string): Promise<Record<string, boolean>> => {
interface HostJSON {
managedDependency?: {
enabled: boolean;
};
version?: string;
extensionBundle?: {
id?: string;
version?: string;
};
}

// we want to eventually move this logic to the backend that way we don't increase save time fetching files
export const getCustomCodeAppFiles = async (
appId?: string,
customCodeFiles?: CustomCodeFileNameMapping
): Promise<Record<string, string>> => {
// only powershell files have custom app files
// to reduce the number of requests, we only check if there are any modified powershell files
if (!customCodeFiles || !Object.values(customCodeFiles).some((file) => file.isModified && file.fileExtension === '.ps1')) {
return {};
}
const appFiles: Record<string, string> = {};
const uri = `${baseUrl}${appId}/hostruntime/admin/vfs`;
const vfsObjects: VFSObject[] = await fetchFilesFromFolder(uri);
return getAppFiles(vfsObjects);
if (vfsObjects.find((file) => file.name === 'host.json')) {
try {
const response = await fetchFileData<HostJSON>(`${uri}/host.json`);
if (!response.managedDependency?.enabled) {
response.managedDependency = {
enabled: true,
};
appFiles['host.json'] = JSON.stringify(response, null, 2);
}
} catch (error) {
const errorMessage = `Failed to parse Host.json: ${error}`;
LoggerService().log({
level: LogEntryLevel.Error,
area: 'serializeCustomcode',
message: errorMessage,
error: error instanceof Error ? error : undefined,
});
}
}
if (!vfsObjects.find((file) => file.name === 'requirements.psd1')) {
appFiles['requirements.psd1'] = getAppFileForFileExtension('.ps1');
}
return appFiles;
};

const getAllCustomCodeFiles = async (appId?: string, workflowName?: string): Promise<Record<string, string>> => {
Expand All @@ -60,7 +104,7 @@ const getAllCustomCodeFiles = async (appId?: string, workflowName?: string): Pro

const filesData = await Promise.all(
vfsObjects.map(async (file) => {
const response = await fetchFileData(`${uri}/${file.name}`);
const response = await fetchFileData<string>(`${uri}/${file.name}`);
return { name: file.name, data: response };
})
);
Expand Down Expand Up @@ -309,9 +353,7 @@ export const saveCustomCodeStandard = async (allCustomCodeFiles?: AllCustomCodeF
});
}
});
Object.entries(appFiles ?? {}).forEach(([fileExtension, fileData]) =>
CustomCodeService().uploadCustomCodeAppFile({ fileName: mapFileExtensionToAppFileName(fileExtension), fileData })
);
Object.entries(appFiles ?? {}).forEach(([fileName, fileData]) => CustomCodeService().uploadCustomCodeAppFile({ fileName, fileData }));
} catch (error) {
const errorMessage = `Failed to save custom code: ${error}`;
LoggerService().log({
Expand Down Expand Up @@ -363,6 +405,7 @@ export const saveWorkflowStandard = async (

// saving custom code must happen synchronously with deploying the workflow artifacts as they both cause
// the host to go soft restart. We may need to look into if there's a race case where this may still happen
// eventually we want to move this logic to the backend to happen with deployWorkflowArtifacts
saveCustomCodeStandard(customCodeData);
const response = await axios.post(`${baseUrl}${siteResourceId}/deployWorkflowArtifacts?api-version=${standardApiVersion}`, data, {
headers: {
Expand Down
Expand Up @@ -18,9 +18,9 @@ export const fetchFilesFromFolder = async (uri: string): Promise<VFSObject[]> =>
).data;
};

export const fetchFileData = async (uri: string): Promise<string> => {
export const fetchFileData = async <T>(uri: string): Promise<T> => {
return (
await axios.get<string>(uri, {
await axios.get<T>(uri, {
headers: {
Authorization: `Bearer ${environment.armToken}`,
'If-Match': ['*'],
Expand Down
Expand Up @@ -40,7 +40,6 @@ import {
StandardSearchService,
clone,
equals,
getAppFileForFileExtension,
guid,
isArmResourceId,
optional,
Expand Down Expand Up @@ -725,28 +724,19 @@ const getCustomCodeToUpdate = async (
appId?: string
): Promise<AllCustomCodeFiles | undefined> => {
const filteredCustomCodeMapping: CustomCodeFileNameMapping = {};
const appFilesToAdd: Record<string, string> = {};
if (!customCode || Object.keys(customCode).length === 0) {
return;
}
const appFiles = await getCustomCodeAppFiles(appId);
const appFiles = await getCustomCodeAppFiles(appId, customCode);

Object.entries(customCode).forEach(([fileName, customCodeData]) => {
const { isModified, isDeleted, fileExtension } = customCodeData;
const { isModified, isDeleted } = customCodeData;

if (isDeleted && originalCustomCodeData.includes(fileName)) {
filteredCustomCodeMapping[fileName] = { ...customCodeData };
} else if (isModified && !isDeleted) {
if (!appFiles[fileExtension] && !appFilesToAdd[fileExtension]) {
const appFileData = getAppFileForFileExtension(fileExtension);
if (appFileData) {
appFilesToAdd[fileExtension] = appFileData;
}
}
if ((isDeleted && originalCustomCodeData.includes(fileName)) || (isModified && !isDeleted)) {
filteredCustomCodeMapping[fileName] = { ...customCodeData };
}
});
return { customCodeFiles: filteredCustomCodeMapping, appFiles: appFilesToAdd };
return { customCodeFiles: filteredCustomCodeMapping, appFiles };
};

export default DesignerEditor;
2 changes: 2 additions & 0 deletions libs/designer/src/lib/common/models/customcode.ts
Expand Up @@ -13,5 +13,7 @@ export type CustomCodeFileNameMapping = Record<string, CustomCodeWithData>;

export interface AllCustomCodeFiles {
customCodeFiles: CustomCodeFileNameMapping;
// appFiles will be stored as [fileName: fileData]
// note app files are stored at the app level and not the workflow level
appFiles: Record<string, string>;
}
4 changes: 4 additions & 0 deletions libs/designer/src/lib/core/actions/bjsworkflow/add.ts
Expand Up @@ -29,6 +29,7 @@ import { isConnectionRequiredForOperation, updateNodeConnection } from './connec
import {
getInputParametersFromManifest,
getOutputParametersFromManifest,
initializeCustomCodeDataInInputs,
updateAllUpstreamNodes,
updateInvokerSettings,
} from './initialize';
Expand Down Expand Up @@ -127,6 +128,9 @@ export const initializeOperationDetails = async (
const iconUri = getIconUriFromManifest(manifest);
const brandColor = getBrandColorFromManifest(manifest);
const { inputs: nodeInputs, dependencies: inputDependencies } = getInputParametersFromManifest(nodeId, manifest, presetParameterValues);
if (equals(operationInfo.connectorId, Constants.INLINECODE) && !equals(operationInfo.operationId, 'javascriptcode')) {
initializeCustomCodeDataInInputs(nodeInputs, nodeId, dispatch);
}
const { outputs: nodeOutputs, dependencies: outputDependencies } = getOutputParametersFromManifest(
manifest,
isTrigger,
Expand Down
27 changes: 27 additions & 0 deletions libs/designer/src/lib/core/actions/bjsworkflow/initialize.ts
Expand Up @@ -47,6 +47,7 @@ import type {
OutputParameter,
SchemaProperty,
SwaggerParser,
EditorLanguage,
} from '@microsoft/logic-apps-shared';
import {
WorkflowService,
Expand Down Expand Up @@ -75,9 +76,13 @@ import {
unmap,
UnsupportedException,
isNullOrEmpty,
generateDefaultCustomCodeValue,
getFileExtensionName,
replaceWhiteSpaceWithUnderscore,
} from '@microsoft/logic-apps-shared';
import type { OutputToken, ParameterInfo } from '@microsoft/designer-ui';
import type { Dispatch } from '@reduxjs/toolkit';
import { addOrUpdateCustomCode } from '../../state/customcode/customcodeSlice';

export interface ServiceOptions {
connectionService: IConnectionService;
Expand Down Expand Up @@ -446,6 +451,28 @@ export const updateCallbackUrlInInputs = async (
return;
};

export const initializeCustomCodeDataInInputs = (inputs: NodeInputs, nodeId: string, dispatch: Dispatch) => {
const parameter = getParameterFromName(inputs, Constants.DEFAULT_CUSTOM_CODE_INPUT);
const language: EditorLanguage = parameter?.editorOptions?.language;
if (parameter) {
const fileData = generateDefaultCustomCodeValue(language);
if (fileData) {
parameter.editorViewModel = {
customCodeData: { fileData },
};
parameter.value = [createLiteralValueSegment(replaceWhiteSpaceWithUnderscore(nodeId) + getFileExtensionName(language))];
dispatch(
addOrUpdateCustomCode({
nodeId,
fileData,
fileExtension: getFileExtensionName(language),
fileName: replaceWhiteSpaceWithUnderscore(nodeId),
})
);
}
}
};

export const updateCustomCodeInInputs = async (
nodeId: string,
fileExtension: string,
Expand Down
72 changes: 57 additions & 15 deletions libs/logic-apps-shared/src/utils/src/lib/helpers/customcode.ts
Expand Up @@ -46,24 +46,66 @@ export const getFileExtensionNameFromOperationId = (operationId: string): string
}
};

export const mapFileExtensionToAppFileName = (fileExtension: string) => {
switch (fileExtension) {
case '.ps1':
return 'requirements.psd1';
default:
return '';
}
};

export const getAppFiles = (files: VFSObject[]): Record<string, boolean> => {
const appFiles: Record<string, boolean> = {};
appFiles['.ps1'] = !!files.find((file) => file.name === 'requirements.psd1');
return appFiles;
};

export const getAppFileForFileExtension = (fileExtension: string): string => {
if (fileExtension === '.ps1') {
return "# This file enables modules to be automatically managed by the Functions service.\r\n# See https://aka.ms/functionsmanageddependency for additional information.\r\n#\r\n@{\r\n # For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'. Uncomment the next line and replace the MAJOR_VERSION, e.g., 'Az' = '5.*'\r\n 'Az' = '10.*'\r\n}";
}
return '';
};

export const generateDefaultCustomCodeValue = (language: EditorLanguage): string => {
switch (language) {
case EditorLanguage.powershell:
return `$action = Get-ActionOutput -actionName "Compose"
$subId = $action["body"]["subscriptionId"]
$resourceGroupName = $action["body"]["resourceGroupName"]
$logicAppName = $action["body"]["logicAppName"]
$result = Start-AzLogicApp -ResourceGroupName $resourceGroupName -Name $logicAppName -TriggerName "manual" -Confirm
Push-ActionOutputs -body $result`;
case EditorLanguage.csharp:
return `// Add the required libraries
const Newtonsoft = require("Newtonsoft.Json");
const AzureScripting = require("Microsoft.Azure.Workflows.Scripting");
// Define the function to run
async function run(context) {
// Get the outputs from the 'compose' action
const outputs = (await context.GetActionResults("compose")).Outputs;
// Generate random temperature within a range based on the temperature scale
const temperatureScale = outputs["temperatureScale"].toString();
const currentTemp = temperatureScale === "Celsius" ? Math.floor(Math.random() * (30 - 1 + 1)) + 1 : Math.floor(Math.random() * (90 - 40 + 1)) + 40;
const lowTemp = currentTemp - 10;
const highTemp = currentTemp + 10;
// Create a Weather object with the temperature information
const weather = {
ZipCode: parseInt(outputs["zipCode"]),
CurrentWeather: \`The current weather is \${currentTemp} \${temperatureScale}\`,
DayLow: \`The low for the day is \${lowTemp} \${temperatureScale}\`,
DayHigh: \`The high for the day is \${highTemp} \${temperatureScale}\`
};
return weather;
}
// Define the Weather class
class Weather {
constructor() {
this.ZipCode = 0;
this.CurrentWeather = "";
this.DayLow = "";
this.DayHigh = "";
}
}
module.exports = run;`;
default:
return '';
}
};

0 comments on commit e911432

Please sign in to comment.