From dbc70e95801ba085cbacb01bde4e25c153f4bbb2 Mon Sep 17 00:00:00 2001 From: DanielleCogs <99753874+DanielleCogs@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:55:19 +0200 Subject: [PATCH 01/11] getting started (#3443) Co-authored-by: Elaina-Lee <144840522+Elaina-Lee@users.noreply.github.com> --- .../app/commands/dataMapper/DataMapperExt.ts | 127 ++++++ .../commands/dataMapper/DataMapperPanel.ts | 410 ++++++++++++++++++ .../commands/dataMapper/FxWorkflowRuntime.ts | 154 +++++++ .../src/app/commands/dataMapper/dataMapper.ts | 171 ++++++++ .../commands/dataMapper/extensionConfig.ts | 48 ++ .../src/app/commands/registerCommands.ts | 6 + 6 files changed, 916 insertions(+) create mode 100644 apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts create mode 100644 apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts create mode 100644 apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts create mode 100644 apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts create mode 100644 apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts new file mode 100644 index 00000000000..a53a8770ddc --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts @@ -0,0 +1,127 @@ +import DataMapperPanel from './DataMapperPanel'; +import { startBackendRuntime } from './FxWorkflowRuntime'; +import { webviewType } from './extensionConfig'; +import type { MapDefinitionData, MapDefinitionEntry } from '@microsoft/logic-apps-data-mapper'; +import type { IAzExtOutputChannel } from '@microsoft/vscode-azext-utils'; +import type { ChildProcess } from 'child_process'; +import * as yaml from 'js-yaml'; +import * as path from 'path'; +import type { ExtensionContext } from 'vscode'; +import { Uri, ViewColumn, window, workspace } from 'vscode'; + +type DataMapperPanelDictionary = { [key: string]: DataMapperPanel }; // key == dataMapName + +export default class DataMapperExt { + public static context: ExtensionContext; + public static extensionPath: string; + public static outputChannel: IAzExtOutputChannel; + public static backendRuntimePort: number; + public static backendRuntimeChildProcess: ChildProcess | undefined; + + public static panelManagers: DataMapperPanelDictionary = {}; + + public static async openDataMapperPanel(dataMapName: string, mapDefinitionData?: MapDefinitionData) { + const workflowFolder = DataMapperExt.getWorkspaceFolderFsPath(); + + if (workflowFolder) { + await startBackendRuntime(workflowFolder); + + DataMapperExt.createOrShow(dataMapName, mapDefinitionData); + } + } + + public static createOrShow(dataMapName: string, mapDefinitionData?: MapDefinitionData) { + // If a panel has already been created, re-show it + if (DataMapperExt.panelManagers[dataMapName]) { + // NOTE: Shouldn't need to re-send runtime port if webview has already been loaded/set up + + window.showInformationMessage(`A Data Mapper panel is already open for this data map (${dataMapName}).`); + DataMapperExt.panelManagers[dataMapName].panel.reveal(ViewColumn.Active); + return; + } + + const panel = window.createWebviewPanel( + webviewType, // Key used to reference the panel + dataMapName, // Title displayed in the tab + ViewColumn.Active, // Editor column to show the new webview panel in + { + enableScripts: true, + // NOTE: Keeps webview content state even when placed in background (same as browsers) + // - not as performant as vscode's get/setState, but likely not a concern at all for MVP + retainContextWhenHidden: true, + } + ); + + DataMapperExt.panelManagers[dataMapName] = new DataMapperPanel(panel, dataMapName); + DataMapperExt.panelManagers[dataMapName].panel.iconPath = { + light: Uri.file(path.join(DataMapperExt.context.extensionPath, 'assets', 'wand-light.png')), + dark: Uri.file(path.join(DataMapperExt.context.extensionPath, 'assets', 'wand-dark.png')), + }; + DataMapperExt.panelManagers[dataMapName].updateWebviewPanelTitle(); + DataMapperExt.panelManagers[dataMapName].mapDefinitionData = mapDefinitionData; + + // From here, VSIX will handle any other initial-load-time events once receive webviewLoaded msg + } + + public static log(text: string) { + DataMapperExt.outputChannel.appendLine(text); + DataMapperExt.outputChannel.show(); + } + + public static showWarning(errMsg: string) { + DataMapperExt.log(errMsg); + window.showWarningMessage(errMsg); + } + + public static showError(errMsg: string) { + DataMapperExt.log(errMsg); + window.showErrorMessage(errMsg); + } + + public static getWorkspaceFolderFsPath() { + if (workspace.workspaceFolders) { + return workspace.workspaceFolders[0].uri.fsPath; + } else { + DataMapperExt.showError('No VS Code folder/workspace found...'); + return ''; + } + } + + /* + Note: This method is copied from the MapDefinition.Utils.ts file in the @microsoft/logic-apps-data-mapper package + if this method gets updated, both need to be updated to keep them in sync. This exists as a copy to avoid a + package import issue. + */ + public static loadMapDefinition = (mapDefinitionString: string | undefined): MapDefinitionEntry => { + if (mapDefinitionString) { + // Add extra escapes around custom string values, so that we don't lose which ones are which + const modifiedMapDefinitionString = mapDefinitionString.replaceAll('"', `\\"`); + const mapDefinition = yaml.load(modifiedMapDefinitionString) as MapDefinitionEntry; + + // Now that we've parsed the yml, remove the extra escaped quotes to restore the values + DataMapperExt.fixMapDefinitionCustomValues(mapDefinition); + + return mapDefinition; + } else { + return {}; + } + }; + + static fixMapDefinitionCustomValues = (mapDefinition: MapDefinitionEntry) => { + for (const key in mapDefinition) { + const curElement = mapDefinition[key]; + if (typeof curElement === 'object' && curElement !== null) { + if (Array.isArray(curElement)) { + // TODO: Handle arrays better, currently fine for XML, but this will need to be re-addressed + // when we get to the advanced JSON array functionality + curElement.forEach((arrayElement) => DataMapperExt.fixMapDefinitionCustomValues(arrayElement)); + } else { + DataMapperExt.fixMapDefinitionCustomValues(curElement); + } + } else if (Object.prototype.hasOwnProperty.call(mapDefinition, key) && typeof curElement === 'string') { + // eslint-disable-next-line no-param-reassign + mapDefinition[key] = curElement.replaceAll('\\"', '"'); + } + } + }; +} diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts new file mode 100644 index 00000000000..a74396815b7 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts @@ -0,0 +1,410 @@ +import DataMapperExt from './DataMapperExt'; +import { + dataMapDefinitionsPath, + dataMapsPath, + draftMapDefinitionSuffix, + mapDefinitionExtension, + mapXsltExtension, + schemasPath, + customXsltPath, + supportedSchemaFileExts, + supportedCustomXsltFileExts, +} from './extensionConfig'; +import type { MapDefinitionData, MessageToVsix, MessageToWebview, SchemaType, MapMetadata } from '@microsoft/logic-apps-data-mapper'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import { callWithTelemetryAndErrorHandlingSync } from '@microsoft/vscode-azext-utils'; +import { + copyFileSync, + existsSync as fileExistsSync, + promises as fs, + unlinkSync as removeFileSync, + statSync, + readdirSync, + readFileSync, +} from 'fs'; +import * as path from 'path'; +import type { WebviewPanel } from 'vscode'; +import { RelativePattern, Uri, window, workspace } from 'vscode'; + +export default class DataMapperPanel { + public panel: WebviewPanel; + + public dataMapName: string; + public dataMapStateIsDirty: boolean; + public mapDefinitionData: MapDefinitionData | undefined; + + constructor(panel: WebviewPanel, dataMapName: string) { + this.panel = panel; + this.dataMapName = dataMapName; + this.dataMapStateIsDirty = false; + this.handleReadSchemaFileOptions = this.handleReadSchemaFileOptions.bind(this); // Bind these as they're used as callbacks + this._handleWebviewMsg = this._handleWebviewMsg.bind(this); + + DataMapperExt.context.subscriptions.push(panel); + + this._setWebviewHtml(); + + // watch folder for file changes + const schemaFolderWatcher = this.watchFolderForChanges(schemasPath, supportedSchemaFileExts, this.handleReadSchemaFileOptions); + const customXsltFolderWatcher = this.watchFolderForChanges( + customXsltPath, + supportedCustomXsltFileExts, + this.handleReadAvailableFunctionPaths + ); + + // Handle messages from the webview (Data Mapper component) + this.panel.webview.onDidReceiveMessage(this._handleWebviewMsg, undefined, DataMapperExt.context.subscriptions); + + this.panel.onDidDispose( + () => { + delete DataMapperExt.panelManagers[this.dataMapName]; + if (schemaFolderWatcher) schemaFolderWatcher.dispose(); + if (customXsltFolderWatcher) customXsltFolderWatcher.dispose(); + }, + null, + DataMapperExt.context.subscriptions + ); + } + + private watchFolderForChanges(folderPath: string, fileExtensions: string[], fn: () => void) { + // Watch folder for changes to update available file list within Data Mapper + const absoluteFolderPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), folderPath); + if (fileExistsSync(absoluteFolderPath)) { + const folderWatcher = workspace.createFileSystemWatcher(new RelativePattern(absoluteFolderPath, `**/*.{${fileExtensions.join()}}`)); + folderWatcher.onDidCreate(fn); + folderWatcher.onDidDelete(fn); + return folderWatcher; + } + return; + } + + private async _setWebviewHtml() { + // Get webview content, converting links to VS Code URIs + const indexPath = path.join(DataMapperExt.extensionPath, '/webview/index.html'); + const html = await fs.readFile(indexPath, 'utf-8'); + // 1. Get all links prefixed by href or src + const matchLinks = /(href|src)="([^"]*)"/g; + // 2. Transform the result of the regex into a vscode's URI format + const toUri = (_: unknown, prefix: 'href' | 'src', link: string) => { + // For HTML elements + if (link === '#') { + return `${prefix}="${link}"`; + } + // For scripts & links + const pth = path.join(DataMapperExt.extensionPath, '/webview/', link); + const uri = Uri.file(pth); + return `${prefix}="${this.panel.webview.asWebviewUri(uri)}"`; + }; + + this.panel.webview.html = html.replace(matchLinks, toUri); + } + + public sendMsgToWebview(msg: MessageToWebview) { + this.panel.webview.postMessage(msg); + } + + private _handleWebviewMsg(msg: MessageToVsix) { + switch (msg.command) { + case 'webviewLoaded': + // Send runtime port to webview + this.sendMsgToWebview({ command: 'setRuntimePort', data: `${DataMapperExt.backendRuntimePort}` }); + + // If loading a data map, handle that + xslt filename + this.handleLoadMapDefinitionIfAny(); + + break; + case 'webviewRscLoadError': + // Handle DM top-level errors (such as loading schemas added from file, or general function manifest fetching issues) + DataMapperExt.showError(`Error loading Data Mapper resource: ${msg.data}`); + break; + case 'addSchemaFromFile': { + this.addSchemaFromFile(msg.data.path, msg.data.type); + break; + } + case 'readLocalSchemaFileOptions': { + this.handleReadSchemaFileOptions(); + break; + } + case 'readLocalCustomXsltFileOptions': { + this.handleReadAvailableFunctionPaths(); + break; + } + case 'saveDataMapDefinition': { + this.saveMapDefinition(msg.data); + break; + } + case 'saveDataMapMetadata': { + this.saveMapMetadata(msg.data); + break; + } + case 'saveDataMapXslt': { + this.saveMapXslt(msg.data); + break; + } + case 'saveDraftDataMapDefinition': { + if (this.dataMapStateIsDirty) { + this.saveDraftDataMapDefinition(msg.data); + } + break; + } + case 'setIsMapStateDirty': { + this.handleUpdateMapDirtyState(msg.data); + break; + } + case 'getFunctionDisplayExpanded': { + this.getConfigurationSetting('useExpandedFunctionCards'); + break; + } + } + } + + public updateWebviewPanelTitle() { + this.panel.title = `${this.dataMapName ?? 'Untitled'} ${this.dataMapStateIsDirty ? '●' : ''}`; + } + + public handleUpdateMapDirtyState(isMapStateDirty: boolean) { + this.dataMapStateIsDirty = isMapStateDirty; + this.updateWebviewPanelTitle(); + + // If map updates to not be dirty (Discard/undo/etc), we can safely remove the draft file + if (!isMapStateDirty) { + this.deleteDraftDataMapDefinition(); + } + } + + public handleLoadMapDefinitionIfAny() { + if (this.mapDefinitionData) { + const mapMetadata = this.readMapMetadataFile(); + this.sendMsgToWebview({ + command: 'loadDataMap', + data: { ...this.mapDefinitionData, metadata: mapMetadata }, + }); + + this.checkAndSetXslt(); + + this.mapDefinitionData = undefined; + } + } + + public getNestedFilePaths(fileName: string, parentPath: string, relativePath: string, filesToDisplay: string[], filetypes: string[]) { + const rootPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), relativePath); + const absolutePath = path.join(rootPath, parentPath, fileName); + if (statSync(absolutePath).isDirectory()) { + readdirSync(absolutePath).forEach((childFileName) => { + const combinedRelativePath = path.join(parentPath, fileName); + this.getNestedFilePaths(childFileName, combinedRelativePath, relativePath, filesToDisplay, filetypes); + }); + } else { + const fileExt = path.extname(fileName).toLowerCase(); + if (filetypes.includes(fileExt)) { + const relativePath = path.join(parentPath, fileName); + filesToDisplay.push(relativePath); + } + } + } + + public handleReadSchemaFileOptions() { + return this.getFilesForPath(schemasPath, 'showAvailableSchemas', supportedSchemaFileExts); + } + + public handleReadAvailableFunctionPaths() { + const absoluteFolderPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), customXsltPath); + if (fileExistsSync(absoluteFolderPath)) { + return this.getFilesForPath(customXsltPath, 'getAvailableCustomXsltPaths', supportedCustomXsltFileExts); + } + } + + private getFilesForPath(folderPath: string, command: 'showAvailableSchemas' | 'getAvailableCustomXsltPaths', fileTypes: string[]) { + fs.readdir(path.join(DataMapperExt.getWorkspaceFolderFsPath(), folderPath)).then((result) => { + const filesToDisplay: string[] = []; + result.forEach((file) => { + this.getNestedFilePaths(file, '', folderPath, filesToDisplay, fileTypes); + }), + this.sendMsgToWebview({ + command, + data: filesToDisplay, + }); + }); + } + + public addSchemaFromFile(filePath: string, schemaType: SchemaType) { + callWithTelemetryAndErrorHandlingSync('azureDataMapper.addSchemaFromFile', (_context: IActionContext) => { + fs.readFile(filePath, 'utf8').then((text: string) => { + const primarySchemaFileName = path.basename(filePath); // Ex: inpSchema.xsd + const expectedPrimarySchemaPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), schemasPath, primarySchemaFileName); + + // Examine the loaded text for the 'schemaLocation' attribute to auto-load in any dependencies too + // NOTE: We only check in the same directory as the primary schema file (also, it doesn't attempt to deal with complicated paths/URLs, just filenames) + const schemaFileDependencies = [...text.matchAll(/schemaLocation="[A-Za-z.]*"/g)].map((schemaFileAttributeMatch) => { + // Trim down to just the filename + return schemaFileAttributeMatch[0].split('"')[1]; + }); + + schemaFileDependencies.forEach((schemaFile) => { + const schemaFilePath = path.join(path.dirname(filePath), schemaFile); + + // Check that the schema file dependency exists in the same directory as the primary schema file + if (!fileExistsSync(schemaFilePath)) { + DataMapperExt.showError( + `Schema loading error: couldn't find schema file dependency ${schemaFile} in the same directory as ${primarySchemaFileName}. ${primarySchemaFileName} will still be copied to the Schemas folder.` + ); + return; + } + + // Check that the schema file dependency doesn't already exist in the Schemas folder + const expectedSchemaFilePath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), schemasPath, schemaFile); + if (!fileExistsSync(expectedSchemaFilePath)) { + copyFileSync(schemaFilePath, expectedSchemaFilePath); + } + }); + + // Check if in Artifacts/Schemas, and if not, create it and send it to DM for API call + if (!fileExistsSync(expectedPrimarySchemaPath)) { + copyFileSync(filePath, expectedPrimarySchemaPath); + } + + this.sendMsgToWebview({ + command: 'fetchSchema', + data: { fileName: primarySchemaFileName, type: schemaType as SchemaType }, + }); + }); + }); + } + + public saveMapDefinition(mapDefinition: string) { + callWithTelemetryAndErrorHandlingSync('azureDataMapper.saveMapDefinition', (_context: IActionContext) => { + // Delete *draft* map definition as it's no longer needed + this.deleteDraftDataMapDefinition(); + + const fileName = `${this.dataMapName}${mapDefinitionExtension}`; + const dataMapFolderPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), dataMapDefinitionsPath); + const filePath = path.join(dataMapFolderPath, fileName); + + // Mkdir as extra insurance that directory exists so file can be written + // - harmless if directory already exists + fs.mkdir(dataMapFolderPath, { recursive: true }) + .then(() => { + fs.writeFile(filePath, mapDefinition, 'utf8').then(() => { + // If XSLT, show notification and re-check/set xslt filename + const openMapBtnText = `Open ${fileName}`; + window.showInformationMessage('Map saved', openMapBtnText).then((clickedButton?: string) => { + if (clickedButton && clickedButton === openMapBtnText) { + workspace.openTextDocument(filePath).then(window.showTextDocument); + } + }); + }); + }) + .catch(DataMapperExt.showError); + }); + } + + public saveMapMetadata(mapMetadata: string) { + const vscodeFolderPath = this.getMapMetadataPath(); + + fs.writeFile(vscodeFolderPath, mapMetadata, 'utf8').catch(DataMapperExt.showError); + } + + public saveMapXslt(mapXslt: string) { + callWithTelemetryAndErrorHandlingSync('azureDataMapper.saveMapXslt', (_context: IActionContext) => { + const fileName = `${this.dataMapName}${mapXsltExtension}`; + const dataMapFolderPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), dataMapsPath); + const filePath = path.join(dataMapFolderPath, fileName); + + // Mkdir as extra insurance that directory exists so file can be written + // - harmless if directory already exists + fs.mkdir(dataMapFolderPath, { recursive: true }) + .then(() => { + fs.writeFile(filePath, mapXslt, 'utf8').then(() => { + const openMapBtnText = `Open ${fileName}`; + window.showInformationMessage('Map XSLT generated.', openMapBtnText).then((clickedButton?: string) => { + if (clickedButton && clickedButton === openMapBtnText) { + workspace.openTextDocument(filePath).then(window.showTextDocument); + } + }); + + this.checkAndSetXslt(); + }); + }) + .catch(DataMapperExt.showError); + }); + } + + public saveDraftDataMapDefinition(mapDefFileContents: string) { + const mapDefileName = `${this.dataMapName}${draftMapDefinitionSuffix}${mapDefinitionExtension}`; + const dataMapDefFolderPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), dataMapDefinitionsPath); + const filePath = path.join(dataMapDefFolderPath, mapDefileName); + + // Mkdir as extra insurance that directory exists so file can be written + // Harmless if directory already exists + fs.mkdir(dataMapDefFolderPath, { recursive: true }) + .then(() => { + fs.writeFile(filePath, mapDefFileContents, 'utf8'); + }) + .catch(DataMapperExt.showError); + } + + private readMapMetadataFile(): MapMetadata | undefined { + const vscodeFolderPath = this.getMapMetadataPath(); + if (fileExistsSync(vscodeFolderPath)) { + try { + const fileBuffer = readFileSync(vscodeFolderPath); + const metadataJson = JSON.parse(fileBuffer.toString()) as MapMetadata; + return metadataJson; + } catch { + DataMapperExt.showError( + `Data map metadata file found at ${vscodeFolderPath} contains invalid JSON. Data map will load without metadata file.` + ); + return undefined; + } + } else { + DataMapperExt.showWarning( + `Data map metadata not found at path ${vscodeFolderPath}. This file configures your function positioning and other info. Please save your map to regenerate the file.` + ); + return undefined; + } + } + + public deleteDraftDataMapDefinition() { + const draftMapDefinitionPath = path.join( + DataMapperExt.getWorkspaceFolderFsPath(), + dataMapDefinitionsPath, + `${this.dataMapName}${draftMapDefinitionSuffix}${mapDefinitionExtension}` + ); + if (fileExistsSync(draftMapDefinitionPath)) { + removeFileSync(draftMapDefinitionPath); + } + } + + public checkAndSetXslt() { + const expectedXsltPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), dataMapsPath, `${this.dataMapName}${mapXsltExtension}`); + + if (fileExistsSync(expectedXsltPath)) { + fs.readFile(expectedXsltPath, 'utf-8').then((fileContents) => { + this.sendMsgToWebview({ + command: 'setXsltData', + data: { + filename: this.dataMapName, + fileContents, + }, + }); + }); + } else { + DataMapperExt.showWarning(`XSLT file not detected for ${this.dataMapName}`); + } + } + + public getConfigurationSetting(configSetting: string) { + const azureDataMapperConfig = workspace.getConfiguration('azureDataMapper'); + const configValue = azureDataMapperConfig.get(configSetting) ?? true; + this.sendMsgToWebview({ + command: 'getConfigurationSetting', + data: configValue, + }); + } + + private getMapMetadataPath() { + const projectPath = DataMapperExt.getWorkspaceFolderFsPath(); + const vscodeFolderPath = path.join(projectPath, '.vscode', `${this.dataMapName}DataMapMetadata.json`); + return vscodeFolderPath; + } +} diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts new file mode 100644 index 00000000000..61e44942eac --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts @@ -0,0 +1,154 @@ +import DataMapperExt from './DataMapperExt'; +import { + backendRuntimeBaseUrl, + backendRuntimeTimeout, + hostFileContent, + hostFileName, + settingsFileContent, + settingsFileName, + workflowDesignTimeDir, + workflowMgmtApi, +} from './extensionConfig'; +import * as cp from 'child_process'; +import { promises as fs, existsSync as fileExists } from 'fs'; +import merge from 'lodash.merge'; +import fetch from 'node-fetch'; +import * as os from 'os'; +import * as path from 'path'; +import * as portfinder from 'portfinder'; +import { ProgressLocation, Uri, window } from 'vscode'; + +// NOTE: LA Standard ext does this in workflowFolder/workflow-designtime +// For now at least, DM is just going to do everything in workflowFolder + +// NOTE (9/12/2022): It's expected that user will already have the Logic Apps +// (Standard) VS Code extension and that it will already automatically install +// Azure Functions Core Tools (so no need to repeat here) + +export async function startBackendRuntime(projectPath: string): Promise { + const runtimeWorkingDir = path.join(projectPath, workflowDesignTimeDir); + + if (!DataMapperExt.backendRuntimePort) { + DataMapperExt.backendRuntimePort = await portfinder.getPortPromise(); + } + + // Note: Must append operationGroups as it's a valid endpoint to ping + const url = `${backendRuntimeBaseUrl}${DataMapperExt.backendRuntimePort}${workflowMgmtApi}operationGroups`; + + await window.withProgress({ location: ProgressLocation.Notification }, async (progress) => { + progress.report({ message: 'Starting backend runtime, this may take a few seconds...' }); + + if (await isBackendRuntimeUp(url)) { + DataMapperExt.log('Backend runtime is already running'); + return; + } + + const modifiedSettingsFileContent = { ...settingsFileContent }; + modifiedSettingsFileContent.Values.ProjectDirectoryPath = projectPath; + + try { + if (runtimeWorkingDir) { + await createDesignTimeDirectory(runtimeWorkingDir); + await createJsonFile(runtimeWorkingDir, hostFileName, hostFileContent); + await createJsonFile(runtimeWorkingDir, settingsFileName, modifiedSettingsFileContent); + + startBackendRuntimeProcess(runtimeWorkingDir, 'func', 'host', 'start', '--port', `${DataMapperExt.backendRuntimePort}`); + + await waitForBackendRuntimeStartUp(url, new Date().getTime()); + } else { + throw new Error("Workflow folder doesn't exist"); + } + } catch (error) { + window.showErrorMessage('Backend runtime could not be started'); + + const errMsg = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error'; + DataMapperExt.log(`Backend runtime failed to start: ${errMsg}`); + } + }); +} + +async function createDesignTimeDirectory(path: string): Promise { + // Check if directory exists at path, and create it if it doesn't + if (!fileExists(path)) { + // Create directory + await fs.mkdir(path, { recursive: true }); + } +} + +async function createJsonFile( + directoryPath: string, + fileName: string, + fileContent: typeof hostFileContent | typeof settingsFileContent +): Promise { + const filePath: Uri = Uri.file(path.join(directoryPath, fileName)); + + // Create file + if (!fileExists(filePath.fsPath)) { + await fs.writeFile(filePath.fsPath, JSON.stringify(fileContent, null, 2), 'utf-8'); + } + // Else merge new settings into existing file + else { + const fileJson = JSON.parse(await fs.readFile(filePath.fsPath, 'utf-8')); + + await fs.writeFile(filePath.fsPath, JSON.stringify(merge(fileJson, fileContent), null, 2), 'utf-8'); + } +} + +async function waitForBackendRuntimeStartUp(url: string, initialTime: number): Promise { + while (!(await isBackendRuntimeUp(url)) && new Date().getTime() - initialTime < backendRuntimeTimeout) { + await delay(1000); // Re-poll every X ms + } + + if (await isBackendRuntimeUp(url)) { + return Promise.resolve(); + } else { + return Promise.reject(); + } +} + +async function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function isBackendRuntimeUp(url: string): Promise { + try { + await fetch(url); + return Promise.resolve(true); + } catch (ex) { + return Promise.resolve(false); + } +} + +function startBackendRuntimeProcess(workingDirectory: string | undefined, command: string, ...args: string[]): void { + const formattedArgs: string = args.join(' '); + const options: cp.SpawnOptions = { + cwd: workingDirectory || os.tmpdir(), + shell: true, + }; + + DataMapperExt.log(`Running command: "${command} ${formattedArgs}"...`); + DataMapperExt.backendRuntimeChildProcess = cp.spawn(command, args, options); + + DataMapperExt.backendRuntimeChildProcess.stdout?.on('data', (data: string | Buffer) => { + DataMapperExt.outputChannel.append(data.toString()); + }); + + DataMapperExt.backendRuntimeChildProcess.stderr?.on('data', (data: string | Buffer) => { + DataMapperExt.outputChannel.append(data.toString()); + }); +} + +// Note: Per node, child processes may not be killed - if this is an issue in the future, a workaround is needed +// HOWEVER - killing the parent process (the VS Code instance?) kills the child process for sure +export function stopBackendRuntime(): void { + if (DataMapperExt.backendRuntimeChildProcess === null || DataMapperExt.backendRuntimeChildProcess === undefined) { + return; + } + + if (os.platform() === 'win32') { + cp.exec('taskkill /pid ' + `${DataMapperExt.backendRuntimeChildProcess.pid}` + ' /T /F'); + } else { + DataMapperExt.backendRuntimeChildProcess.kill(); + } + DataMapperExt.backendRuntimeChildProcess = undefined; +} diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts b/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts new file mode 100644 index 00000000000..bc6e9fc26e9 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts @@ -0,0 +1,171 @@ +/* eslint-disable no-param-reassign */ +import DataMapperExt from './DataMapperExt'; +import { dataMapDefinitionsPath, draftMapDefinitionSuffix, schemasPath, supportedDataMapDefinitionFileExts } from './extensionConfig'; +import type { MapDefinitionEntry } from '@microsoft/logic-apps-data-mapper'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; +import { existsSync as fileExistsSync, promises as fs } from 'fs'; +import * as path from 'path'; +import { Uri, window } from 'vscode'; + +export const createNewDataMapCmd = (context: IActionContext) => { + window.showInputBox({ prompt: 'Data Map name: ' }).then(async (newDataMapName) => { + if (!newDataMapName) { + context.telemetry.properties.result = 'Canceled'; + return; + } + + context.telemetry.properties.result = 'Succeeded'; + + DataMapperExt.openDataMapperPanel(newDataMapName); + }); +}; + +export const loadDataMapFileCmd = async (context: IActionContext, uri: Uri) => { + let mapDefinitionPath: string | undefined = uri?.fsPath; + let draftFileIsFoundAndShouldBeUsed = false; + + // Handle if Uri isn't provided/defined (cmd pallette or btn) + if (!mapDefinitionPath) { + const fileUris = await window.showOpenDialog({ + title: 'Select a data map definition to load', + defaultUri: Uri.file(path.join(DataMapperExt.getWorkspaceFolderFsPath(), dataMapDefinitionsPath)), + canSelectMany: false, + canSelectFiles: true, + canSelectFolders: false, + filters: { 'Data Map Definition': supportedDataMapDefinitionFileExts.map((ext) => ext.replace('.', '')) }, + }); + + if (fileUris && fileUris.length > 0) { + mapDefinitionPath = fileUris[0].fsPath; + } else { + context.telemetry.properties.result = 'Canceled'; + context.telemetry.properties.wasUsingFilePicker = 'true'; + return; + } + } + + // Check if there's a draft version of the map (more up-to-date version) definition first, and load that if so + const mapDefinitionFileName = path.basename(mapDefinitionPath); + const mapDefFileExt = path.extname(mapDefinitionFileName); + const draftMapDefinitionPath = path.join( + path.dirname(mapDefinitionPath), + mapDefinitionFileName.replace(mapDefFileExt, `${draftMapDefinitionSuffix}${mapDefFileExt}`) + ); + + if (!mapDefinitionFileName.includes(draftMapDefinitionSuffix)) { + // The file we're loading isn't a draft file itself, so now it makes sense to check for a draft version + if (fileExistsSync(draftMapDefinitionPath)) { + draftFileIsFoundAndShouldBeUsed = true; + } + } + + let mapDefinition: MapDefinitionEntry = {}; + // Try to load the draft file first + if (draftFileIsFoundAndShouldBeUsed) { + const fileContents = await fs.readFile(draftMapDefinitionPath, 'utf-8'); + mapDefinition = DataMapperExt.loadMapDefinition(fileContents); + } + + //If there is no draft file, or the draft file fails to deserialize, fall back to the base file + if (Object.keys(mapDefinition).length === 0) { + const fileContents = await fs.readFile(mapDefinitionPath, 'utf-8'); + mapDefinition = DataMapperExt.loadMapDefinition(fileContents); + } + + /* const mapDefinition = yaml.load( + fileContents + ) as { + $sourceSchema: string; + $targetSchema: string; + [key: string]: any; + }; */ + + if ( + !mapDefinition.$sourceSchema || + typeof mapDefinition.$sourceSchema !== 'string' || + !mapDefinition.$targetSchema || + typeof mapDefinition.$targetSchema !== 'string' + ) { + context.telemetry.properties.eventDescription = 'Attempted to load invalid map, missing schema definitions'; + DataMapperExt.showError('Invalid map definition: $sourceSchema and $targetSchema must be defined.'); + return; + } + + // Attempt to load schema files if specified + const schemasFolder = path.join(DataMapperExt.getWorkspaceFolderFsPath(), schemasPath); + const srcSchemaPath = path.join(schemasFolder, mapDefinition.$sourceSchema); + const tgtSchemaPath = path.join(schemasFolder, mapDefinition.$targetSchema); + + const attemptToResolveMissingSchemaFile = async (schemaName: string, schemaPath: string): Promise => { + return !!(await callWithTelemetryAndErrorHandling( + 'azureDataMapper.attemptToResolveMissingSchemaFile', + async (_context: IActionContext) => { + const findSchemaFileButton = 'Find schema file'; + const clickedButton = await window.showErrorMessage( + `Error loading map definition: ${schemaName} was not found in the Schemas folder!`, + findSchemaFileButton + ); + + if (clickedButton && clickedButton === findSchemaFileButton) { + const fileUris = await window.showOpenDialog({ + title: 'Select the missing schema file', + canSelectMany: false, + canSelectFiles: true, + canSelectFolders: false, + filters: { 'XML Schema': ['xsd'], 'JSON Schema': ['json'] }, + }); + + if (fileUris && fileUris.length > 0) { + // Copy the schema file they selected to the Schemas folder (can safely continue map definition loading) + await fs.copyFile(fileUris[0].fsPath, schemaPath); + context.telemetry.properties.result = 'Succeeded'; + + return true; + } + } + + // If user doesn't select a file, or doesn't click the above action, just return (cancel loading the MapDef) + context.telemetry.properties.result = 'Canceled'; + context.telemetry.properties.wasResolvingMissingSchemaFile = 'true'; + + return false; + } + )); + }; + + // If schema file doesn't exist, prompt to find/select it + if (!fileExistsSync(srcSchemaPath)) { + const successfullyFoundAndCopiedSchemaFile = await attemptToResolveMissingSchemaFile(mapDefinition.$sourceSchema, srcSchemaPath); + + if (!successfullyFoundAndCopiedSchemaFile) { + context.telemetry.properties.result = 'Canceled'; + context.telemetry.properties.missingSourceSchema = 'true'; + + DataMapperExt.showError('No source schema file was selected. Aborting load...'); + return; + } + } + + if (!fileExistsSync(tgtSchemaPath)) { + const successfullyFoundAndCopiedSchemaFile = await attemptToResolveMissingSchemaFile(mapDefinition.$targetSchema, tgtSchemaPath); + + if (!successfullyFoundAndCopiedSchemaFile) { + context.telemetry.properties.result = 'Canceled'; + context.telemetry.properties.missingTargetSchema = 'true'; + + DataMapperExt.showError('No target schema file was selected. Aborting load...'); + return; + } + } + + const dataMapName = path.basename(mapDefinitionPath, path.extname(mapDefinitionPath)).replace(draftMapDefinitionSuffix, ''); // Gets filename w/o ext (and w/o draft suffix) + + // Set map definition data to be loaded once webview sends webviewLoaded msg + DataMapperExt.openDataMapperPanel(dataMapName, { + mapDefinition, + sourceSchemaFileName: path.basename(srcSchemaPath), + targetSchemaFileName: path.basename(tgtSchemaPath), + metadata: undefined, + }); +}; diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts b/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts new file mode 100644 index 00000000000..f9728d73b89 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts @@ -0,0 +1,48 @@ +export const webviewType = 'dataMapperWebview'; +export const outputChannelTitle = 'Data Mapper'; +export const outputChannelPrefix = 'azureLogicAppsDataMapper'; + +export const supportedDataMapDefinitionFileExts = ['.yml']; +export const supportedSchemaFileExts = ['.xsd', '.json']; +export const supportedCustomXsltFileExts = ['.xslt']; + +const artifactsPath = '/Artifacts/'; +export const schemasPath = `${artifactsPath}/Schemas`; +export const customXsltPath = `${artifactsPath}/DataMapper/Extensions/InlineXslt`; +export const dataMapsPath = `${artifactsPath}/Maps`; +export const dataMapDefinitionsPath = `${artifactsPath}/MapDefinitions`; +export const workflowDesignTimeDir = '/workflow-designtime'; + +export const defaultDataMapFilename = 'default'; +export const draftMapDefinitionSuffix = '.draft'; +export const mapDefinitionExtension = '.yml'; +export const mapXsltExtension = '.xslt'; + +export const hostFileName = 'host.json'; +export const settingsFileName = 'local.settings.json'; +export const hostFileContent = { + version: '2.0', + extensionBundle: { + id: 'Microsoft.Azure.Functions.ExtensionBundle.Workflows', + version: '[1.*, 2.0.0)', + }, + extensions: { + workflow: { + settings: { + 'Runtime.WorkflowOperationDiscoveryHostMode': 'true', + }, + }, + }, +}; +export const settingsFileContent = { + IsEncrypted: false, + Values: { + AzureWebJobsSecretStorageType: 'Files', + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated', + ProjectDirectoryPath: 'should/be/set/by/code', + }, +}; + +export const backendRuntimeBaseUrl = 'http://localhost:'; +export const workflowMgmtApi = '/runtime/webhooks/workflow/api/management/'; +export const backendRuntimeTimeout = 60000; diff --git a/apps/vs-code-designer/src/app/commands/registerCommands.ts b/apps/vs-code-designer/src/app/commands/registerCommands.ts index 54120d57610..0fa50998dcd 100644 --- a/apps/vs-code-designer/src/app/commands/registerCommands.ts +++ b/apps/vs-code-designer/src/app/commands/registerCommands.ts @@ -19,6 +19,7 @@ import { createLogicApp, createLogicAppAdvanced } from './createLogicApp/createL import { createNewCodeProjectFromCommand } from './createNewCodeProject/createNewCodeProject'; import { createNewProjectFromCommand } from './createNewProject/createNewProject'; import { createSlot } from './createSlot'; +import { createNewDataMapCmd, loadDataMapFileCmd } from './dataMapper/dataMapper'; import { deleteLogicApp } from './deleteLogicApp/deleteLogicApp'; import { deleteNode } from './deleteNode'; import { deployProductionSlot, deploySlot } from './deploy/deploy'; @@ -54,6 +55,7 @@ import { AppSettingsTreeItem, AppSettingTreeItem, registerSiteCommand } from '@m import type { FileTreeItem } from '@microsoft/vscode-azext-azureappservice'; import { registerCommand, registerCommandWithTreeNodeUnwrapping, unwrapTreeNodeCommandCallback } from '@microsoft/vscode-azext-utils'; import type { AzExtTreeItem, IActionContext, AzExtParentTreeItem } from '@microsoft/vscode-azext-utils'; +import type { Uri } from 'vscode'; export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping(extensionCommand.openDesigner, openDesigner); @@ -124,4 +126,8 @@ export function registerCommands(): void { registerCommand(extensionCommand.initProjectForVSCode, initProjectForVSCode); registerCommandWithTreeNodeUnwrapping(extensionCommand.configureDeploymentSource, configureDeploymentSource); registerCommandWithTreeNodeUnwrapping(extensionCommand.startRemoteDebug, startRemoteDebug); + + // Data Mapper Commands + registerCommand('azureDataMapper.createNewDataMap', (context: IActionContext) => createNewDataMapCmd(context)); + registerCommand('azureDataMapper.loadDataMapFile', (context: IActionContext, uri: Uri) => loadDataMapFileCmd(context, uri)); } From 7cc95998dcd94fd6ab2221b3a4ba7f2d197d4315 Mon Sep 17 00:00:00 2001 From: Elaina-Lee <144840522+Elaina-Lee@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:57:09 -0700 Subject: [PATCH 02/11] Move Activate/Deactivate & Data Mapper Extension Variables (#3465) * moved data mapper extension variables * removed DataMapperExt static vals moved to main ext * moved panel managers from data mapper ext to regular ext * moved image assets and changed names accordingly * changed executeCommand dataMapper to constant LogicAppsStandard --- .../app/commands/dataMapper/DataMapperExt.ts | 34 ++++++------------ .../commands/dataMapper/DataMapperPanel.ts | 15 ++++---- .../commands/dataMapper/FxWorkflowRuntime.ts | 29 +++++++-------- .../vs-code-designer/src/assets/dark/wand.png | Bin 0 -> 1785 bytes .../src/assets/light/wand.png | Bin 0 -> 1787 bytes apps/vs-code-designer/src/constants.ts | 3 ++ .../src/extensionVariables.ts | 9 +++++ apps/vs-code-designer/src/main.ts | 14 ++++++++ 8 files changed, 60 insertions(+), 44 deletions(-) create mode 100644 apps/vs-code-designer/src/assets/dark/wand.png create mode 100644 apps/vs-code-designer/src/assets/light/wand.png diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts index a53a8770ddc..e9d4b9a686f 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts @@ -1,25 +1,13 @@ +import { ext } from '../../../extensionVariables'; import DataMapperPanel from './DataMapperPanel'; import { startBackendRuntime } from './FxWorkflowRuntime'; import { webviewType } from './extensionConfig'; import type { MapDefinitionData, MapDefinitionEntry } from '@microsoft/logic-apps-data-mapper'; -import type { IAzExtOutputChannel } from '@microsoft/vscode-azext-utils'; -import type { ChildProcess } from 'child_process'; import * as yaml from 'js-yaml'; import * as path from 'path'; -import type { ExtensionContext } from 'vscode'; import { Uri, ViewColumn, window, workspace } from 'vscode'; -type DataMapperPanelDictionary = { [key: string]: DataMapperPanel }; // key == dataMapName - export default class DataMapperExt { - public static context: ExtensionContext; - public static extensionPath: string; - public static outputChannel: IAzExtOutputChannel; - public static backendRuntimePort: number; - public static backendRuntimeChildProcess: ChildProcess | undefined; - - public static panelManagers: DataMapperPanelDictionary = {}; - public static async openDataMapperPanel(dataMapName: string, mapDefinitionData?: MapDefinitionData) { const workflowFolder = DataMapperExt.getWorkspaceFolderFsPath(); @@ -32,11 +20,11 @@ export default class DataMapperExt { public static createOrShow(dataMapName: string, mapDefinitionData?: MapDefinitionData) { // If a panel has already been created, re-show it - if (DataMapperExt.panelManagers[dataMapName]) { + if (ext.dataMapPanelManagers[dataMapName]) { // NOTE: Shouldn't need to re-send runtime port if webview has already been loaded/set up window.showInformationMessage(`A Data Mapper panel is already open for this data map (${dataMapName}).`); - DataMapperExt.panelManagers[dataMapName].panel.reveal(ViewColumn.Active); + ext.dataMapPanelManagers[dataMapName].panel.reveal(ViewColumn.Active); return; } @@ -52,20 +40,20 @@ export default class DataMapperExt { } ); - DataMapperExt.panelManagers[dataMapName] = new DataMapperPanel(panel, dataMapName); - DataMapperExt.panelManagers[dataMapName].panel.iconPath = { - light: Uri.file(path.join(DataMapperExt.context.extensionPath, 'assets', 'wand-light.png')), - dark: Uri.file(path.join(DataMapperExt.context.extensionPath, 'assets', 'wand-dark.png')), + ext.dataMapPanelManagers[dataMapName] = new DataMapperPanel(panel, dataMapName); + ext.dataMapPanelManagers[dataMapName].panel.iconPath = { + light: Uri.file(path.join(ext.context.extensionPath, 'assets', 'light', 'wand.png')), + dark: Uri.file(path.join(ext.context.extensionPath, 'assets', 'dark', 'wand.png')), }; - DataMapperExt.panelManagers[dataMapName].updateWebviewPanelTitle(); - DataMapperExt.panelManagers[dataMapName].mapDefinitionData = mapDefinitionData; + ext.dataMapPanelManagers[dataMapName].updateWebviewPanelTitle(); + ext.dataMapPanelManagers[dataMapName].mapDefinitionData = mapDefinitionData; // From here, VSIX will handle any other initial-load-time events once receive webviewLoaded msg } public static log(text: string) { - DataMapperExt.outputChannel.appendLine(text); - DataMapperExt.outputChannel.show(); + ext.outputChannel.appendLine(text); + ext.outputChannel.show(); } public static showWarning(errMsg: string) { diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts index a74396815b7..49fada43418 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts @@ -1,3 +1,4 @@ +import { ext } from '../../../extensionVariables'; import DataMapperExt from './DataMapperExt'; import { dataMapDefinitionsPath, @@ -40,7 +41,7 @@ export default class DataMapperPanel { this.handleReadSchemaFileOptions = this.handleReadSchemaFileOptions.bind(this); // Bind these as they're used as callbacks this._handleWebviewMsg = this._handleWebviewMsg.bind(this); - DataMapperExt.context.subscriptions.push(panel); + ext.context.subscriptions.push(panel); this._setWebviewHtml(); @@ -53,16 +54,16 @@ export default class DataMapperPanel { ); // Handle messages from the webview (Data Mapper component) - this.panel.webview.onDidReceiveMessage(this._handleWebviewMsg, undefined, DataMapperExt.context.subscriptions); + this.panel.webview.onDidReceiveMessage(this._handleWebviewMsg, undefined, ext.context.subscriptions); this.panel.onDidDispose( () => { - delete DataMapperExt.panelManagers[this.dataMapName]; + delete ext.dataMapPanelManagers[this.dataMapName]; if (schemaFolderWatcher) schemaFolderWatcher.dispose(); if (customXsltFolderWatcher) customXsltFolderWatcher.dispose(); }, null, - DataMapperExt.context.subscriptions + ext.context.subscriptions ); } @@ -80,7 +81,7 @@ export default class DataMapperPanel { private async _setWebviewHtml() { // Get webview content, converting links to VS Code URIs - const indexPath = path.join(DataMapperExt.extensionPath, '/webview/index.html'); + const indexPath = path.join(ext.context.extensionPath, '/webview/index.html'); const html = await fs.readFile(indexPath, 'utf-8'); // 1. Get all links prefixed by href or src const matchLinks = /(href|src)="([^"]*)"/g; @@ -91,7 +92,7 @@ export default class DataMapperPanel { return `${prefix}="${link}"`; } // For scripts & links - const pth = path.join(DataMapperExt.extensionPath, '/webview/', link); + const pth = path.join(ext.context.extensionPath, '/webview/', link); const uri = Uri.file(pth); return `${prefix}="${this.panel.webview.asWebviewUri(uri)}"`; }; @@ -107,7 +108,7 @@ export default class DataMapperPanel { switch (msg.command) { case 'webviewLoaded': // Send runtime port to webview - this.sendMsgToWebview({ command: 'setRuntimePort', data: `${DataMapperExt.backendRuntimePort}` }); + this.sendMsgToWebview({ command: 'setRuntimePort', data: `${ext.dataMapperRuntimePort}` }); // If loading a data map, handle that + xslt filename this.handleLoadMapDefinitionIfAny(); diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts index 61e44942eac..f62f31bee66 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts @@ -1,3 +1,4 @@ +import { ext } from '../../../extensionVariables'; import DataMapperExt from './DataMapperExt'; import { backendRuntimeBaseUrl, @@ -28,12 +29,12 @@ import { ProgressLocation, Uri, window } from 'vscode'; export async function startBackendRuntime(projectPath: string): Promise { const runtimeWorkingDir = path.join(projectPath, workflowDesignTimeDir); - if (!DataMapperExt.backendRuntimePort) { - DataMapperExt.backendRuntimePort = await portfinder.getPortPromise(); + if (!ext.dataMapperRuntimePort) { + ext.dataMapperRuntimePort = await portfinder.getPortPromise(); } // Note: Must append operationGroups as it's a valid endpoint to ping - const url = `${backendRuntimeBaseUrl}${DataMapperExt.backendRuntimePort}${workflowMgmtApi}operationGroups`; + const url = `${backendRuntimeBaseUrl}${ext.dataMapperRuntimePort}${workflowMgmtApi}operationGroups`; await window.withProgress({ location: ProgressLocation.Notification }, async (progress) => { progress.report({ message: 'Starting backend runtime, this may take a few seconds...' }); @@ -52,7 +53,7 @@ export async function startBackendRuntime(projectPath: string): Promise { await createJsonFile(runtimeWorkingDir, hostFileName, hostFileContent); await createJsonFile(runtimeWorkingDir, settingsFileName, modifiedSettingsFileContent); - startBackendRuntimeProcess(runtimeWorkingDir, 'func', 'host', 'start', '--port', `${DataMapperExt.backendRuntimePort}`); + startBackendRuntimeProcess(runtimeWorkingDir, 'func', 'host', 'start', '--port', `${ext.dataMapperRuntimePort}`); await waitForBackendRuntimeStartUp(url, new Date().getTime()); } else { @@ -127,28 +128,28 @@ function startBackendRuntimeProcess(workingDirectory: string | undefined, comman }; DataMapperExt.log(`Running command: "${command} ${formattedArgs}"...`); - DataMapperExt.backendRuntimeChildProcess = cp.spawn(command, args, options); + ext.dataMapperChildProcess = cp.spawn(command, args, options); - DataMapperExt.backendRuntimeChildProcess.stdout?.on('data', (data: string | Buffer) => { - DataMapperExt.outputChannel.append(data.toString()); + ext.dataMapperChildProcess.stdout?.on('data', (data: string | Buffer) => { + ext.outputChannel.append(data.toString()); }); - DataMapperExt.backendRuntimeChildProcess.stderr?.on('data', (data: string | Buffer) => { - DataMapperExt.outputChannel.append(data.toString()); + ext.dataMapperChildProcess.stderr?.on('data', (data: string | Buffer) => { + ext.outputChannel.append(data.toString()); }); } // Note: Per node, child processes may not be killed - if this is an issue in the future, a workaround is needed // HOWEVER - killing the parent process (the VS Code instance?) kills the child process for sure -export function stopBackendRuntime(): void { - if (DataMapperExt.backendRuntimeChildProcess === null || DataMapperExt.backendRuntimeChildProcess === undefined) { +export function stopDataMapperBackend(): void { + if (ext.dataMapperChildProcess === null || ext.dataMapperChildProcess === undefined) { return; } if (os.platform() === 'win32') { - cp.exec('taskkill /pid ' + `${DataMapperExt.backendRuntimeChildProcess.pid}` + ' /T /F'); + cp.exec('taskkill /pid ' + `${ext.dataMapperChildProcess.pid}` + ' /T /F'); } else { - DataMapperExt.backendRuntimeChildProcess.kill(); + ext.dataMapperChildProcess.kill(); } - DataMapperExt.backendRuntimeChildProcess = undefined; + ext.dataMapperChildProcess = undefined; } diff --git a/apps/vs-code-designer/src/assets/dark/wand.png b/apps/vs-code-designer/src/assets/dark/wand.png new file mode 100644 index 0000000000000000000000000000000000000000..b1acd9b3deed2b0c9bb1a16a455cbcc5ad059529 GIT binary patch literal 1785 zcmV*s)7m_8rzE3^V-ExKq00000000000BxZ|z&sZh7u~hBwNL!A$J+*P zXRltpdf)H&uPG83z&La9r}RVk$K0?9`mrWhAz&I7|CoLk-?4FbclU`RfdT9@EZEnh zNML|^vq6!-04)awXgM%I%Rw4wfEV)oSc2;WFn}@zd44QGbpi|l$`s`Ju>{o#U;urF zB!{<700ZcALvnZ{3k;yoR}$jk;o%?OF))BKx3{+kBohU0R##Ww`3`~sl-b?gy=D{i z`Q;gzmw+nb29WM& zgE(@`0Yv-RAP((GphN|%X-L)ykOkC$rFDXkJl_WlV4Q+FK~SFW0|qe83w450dA^S% zbpXwNc1}2Lb(zbXPN(C(=YT`7`>u=c>q7Mn|LqHJXFEGPVkO>`L>oZ6pPhs8csyXy z59voOBM3IZHHYXU{`@Com}moVrRtRATACxSbnTH6?vB5|zdvL#E048vBkWnNdZw5I zsP?m^aFLD0pT>yq;KoZ6pPh;v;xFLq9Fq~m zp9hfcXQ!Zo_#@s<*rX$8QkN~vX0slL_%arSVuzP^6$n?Q1Au+RUf8=%#t;IiU#TDbRI{O7nKk18iod``0`iA=^=u%A0Yv-RWyvA_0@oBO+RvZk zH7O%*0O@{q8FHq24*q_H#+i2$RS`FUbU!SS200hRgmWZ|B*6{H9&<5Ur4$d zYyvKU?~^Cb=SkuzDbq*;R7v&RMu|)?-xt#G`9jJx&Hz>Oe7B`ALFo0lE&SQSV`7am zKx~mG)&ym)&&Pcr12iTLOj+TpaxV8G^gZtG-`w0hAOkeQ021Zh3X@0*Io8@!!nHSm zWNlxdbZq9g4hw2M2as;;l$;X;UEkX818N%qa(4B!O368>r2%q^U-6pBuP;UI44@%? z#cLL^dFOOLRK>R$IGyufU zhdBd4{JfYl0L0Ia83RCk9ZVPi;_Fej|C|BhCq~r(5MLiP13-Kms2Bj^+abFFAigcK z8UW(kBbxyrzD=?ipzY#YUN=$P01$uadI~0fBOv-rAi}==brZ!50P%k^EZEm0Wkd`B z@q-EZe4->b0K~5sNeuw;>qjyJK>T`=Bn?EWSKoCLqYVJ@>%DGb)DVEMUzq2|lBG^i z*#Hp#Cqt4$yiQQb01*G_`AH7(IzedzK>XhjJ9m!t6iOMuO?V*wQ|i8MVz2?6_--{U zg7{CU`?`t2X&?*mIdDxp6nP$Deci;M2rv)*5Wj&08DO64`hfT~#A^U|_me-@h4?ka zYXH9c2fkx?!txH{FGaFW5KYJs@TX`0DCHu^4ZQQ0em0GD6Q2`efNs%FzWYB?BAOWX zEv=jQ5+MfYdG;q+_N1v}zLj+om*LGpbL6p@@aYl#M9_3PjcQmxd|kW-=yW>QMf+#7 znQw?#3dGmPYXG;ziRX|3RRH4KAY=&mw`eCvh_^fpbg2Xo-wq)Wz}Mw+k1m~t&IKU8 zExZQU-rgSh<^fEPO>pju1e{R}A-*kwQn>MWe8eU=3^??Lk2_=GPMmSvA6#T$a}4<9 zTMC~qK>SPu=kSxs=@Q~+CfEQjq;mv#PjWzf1xi%{*#sZi z1QLrLLwqGF)$vPA3cXlcOSf%Q>IHBlIO03DACMmU_?yKywU%z%i0Ru45^ed zbQrGJ^mdB62_wdUE*qx&iFPr8k}7RK<>At^+y3&PR1ncFez5>3V)hdPrH_tEO(@ojcosAcX$qtlSf~Tp0f3e~ z#qG;-0KoHI2@XC<&`MdCpB@o)M{yoR!|KhRE^d(9N>U%uJ`5T?T`pkDSo!wB&?{h@axUEFbW%Vj=HwWW@{z z9#%w`t=)L)pgviM|53rW>0Bd5TX7LY2w@h7YtW8=TT(qfIv>OVC@LbRaK|*zr(0i|ppZWPqcXFSEl$-rkG5z};hRK;)JDl26 zG*zp-vQK<2{*!uu!iAd(5uCyJ{I&l4*-+@ANos>GwCBDe(v%Cm>fVIRZ|j(T4LcEC zYa%gI8@nj`U;0XKb^VATPepQmQCU|@T#zGp0!>xloG9*rO)%+qIYyShB*L#V_=CmW zFyx=O&1i;ilTyC_ysMQ%p~$vgf>&N7tJ9zUVZ^UHebb__x8~4!t&{ZV^4QOb?#hc! zu)cuV?=cAV9GrMvpn4h$os8H*>N(_xr>atg&DCRQ{EL<`a}{|*Xl8yu|K7vlLgO0t(Ms(aaiwjq#e6?HfItA9Q%^ZFxPcda45QhyT zWbvtXZ#BT2aBd2qg0RowG)YKw7|04zPTa>ILZqm5N{ZI4=79aY%6sn-G{^592Pm%A z*V4~e`cW{qRoMz@GtKU)ORSmrtgq;Z75^f?ZP6I2EoU5^A=d&qEp;2~en$_G##;vk!9 z?@{xLoMR2x)UvQ%DS62Dz859GEKM?A|E71VJ^MVm!XysAt)&t|GkMh@C#_ro&5wDe zUs9b*EUAF59643=#)M5hws6|P(xQa+cqw81EseT478$|Y_*lr=cxI|JNltamfi{*B zhWKKxG3J)imLgGT$aKzu?xyef7264fR@}k%Y@z&z3WIfDcgeB~Q!lH>E!-EMdhv>q zx#IG{0NqI$(F#JHg7%%UG$f}Yv(_@{@*fi^ds}tBhmh<|M|YTfd|QI=%fZ)efW7H7 z47?WnZmZgAU5Hp*nK%}glw^}GVP=uu*&P06!ORH#Z&H`zjW8zaeIG_eO*%xg9;aaa z-4YROGh*QuwCdMs=LMfDF=$uTHX!d)C@O`Q^JZpdC$FkAuUgt_ohcxb!sG1h+-i%= zG1fzIbDiO+)wQ)6Te7wqJPOw=*+Ut7)CYo7G>?U~N!`(Ie+i$uIeK{7Cnjz^Hz_cB zRNt|7suElLt8TO2v}u3jZ8U@O$3d>owfq&q=OM1!Ey_U9+6Qslxcnuqp>z%X6T`&a Y1GT@A%1h$9{r4?bCl5m7SxUzL0g|aRqyPW_ literal 0 HcmV?d00001 diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index dd2b18fb912..10871e16bd5 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -117,6 +117,9 @@ export enum extensionCommand { startRemoteDebug = 'azureLogicAppsStandard.startRemoteDebug', validateLogicAppProjects = 'azureLogicAppsStandard.validateFunctionProjects', reportIssue = 'azureLogicAppsStandard.reportIssue', + dataMapSetSupportedDataMapDefinitionFileExts = 'azureLogicAppsStandard.dataMap.setUpportedDataMapDefinitionFileExts', + dataMapSetSupportedSchemaFileExts = 'azureLogicAppsStandard.dataMap.setSupportedSchemaFileExts', + dataMapSetSupportedFileExts = 'azureLogicAppsStandard.dataMap.setSupportedSupportedFileExts', } // Context diff --git a/apps/vs-code-designer/src/extensionVariables.ts b/apps/vs-code-designer/src/extensionVariables.ts index 23cd484d11b..0115fe00954 100644 --- a/apps/vs-code-designer/src/extensionVariables.ts +++ b/apps/vs-code-designer/src/extensionVariables.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type DataMapperPanel from './app/commands/dataMapper/DataMapperPanel'; import type { AzureAccountTreeItemWithProjects } from './app/tree/AzureAccountTreeItemWithProjects'; import { func } from './constants'; import type { Site } from '@azure/arm-appservice'; @@ -13,11 +14,16 @@ import type { ExtensionContext, WebviewPanel } from 'vscode'; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts */ + +type DataMapperPanelDictionary = { [key: string]: DataMapperPanel }; // key == dataMapName + // eslint-disable-next-line @typescript-eslint/no-namespace export namespace ext { export let context: ExtensionContext; export let workflowDesignTimePort: number; export let workflowDesignChildProcess: cp.ChildProcess | undefined; + export let dataMapperRuntimePort: number; + export let dataMapperChildProcess: cp.ChildProcess | undefined; export let outputChannel: IAzExtOutputChannel; export let workflowRuntimePort: number; export let extensionVersion: string; @@ -32,6 +38,9 @@ export namespace ext { // Resource group API export let rgApi: AzureHostExtensionApi; + // Data Mapper panel + export let dataMapPanelManagers: DataMapperPanelDictionary; + // Functions export const funcCliPath: string = func; diff --git a/apps/vs-code-designer/src/main.ts b/apps/vs-code-designer/src/main.ts index abe5aca95a1..57dd99a01ab 100644 --- a/apps/vs-code-designer/src/main.ts +++ b/apps/vs-code-designer/src/main.ts @@ -1,5 +1,7 @@ import { LogicAppResolver } from './LogicAppResolver'; import { runPostWorkflowCreateStepsFromCache } from './app/commands/createCodeless/createCodelessSteps/WorkflowCreateStepBase'; +import { stopDataMapperBackend } from './app/commands/dataMapper/FxWorkflowRuntime'; +import { supportedDataMapDefinitionFileExts, supportedSchemaFileExts } from './app/commands/dataMapper/extensionConfig'; import { validateFuncCoreToolsIsLatest } from './app/commands/funcCoreTools/validateFuncCoreToolsIsLatest'; import { registerCommands } from './app/commands/registerCommands'; import { getResourceGroupsApi } from './app/resourcesExtension/getExtensionApi'; @@ -29,6 +31,17 @@ const perfStats = { }; export async function activate(context: vscode.ExtensionContext) { + vscode.commands.executeCommand( + 'setContext', + extensionCommand.dataMapSetSupportedDataMapDefinitionFileExts, + supportedDataMapDefinitionFileExts + ); + vscode.commands.executeCommand('setContext', extensionCommand.dataMapSetSupportedSchemaFileExts, supportedSchemaFileExts); + vscode.commands.executeCommand('setContext', extensionCommand.dataMapSetSupportedFileExts, [ + ...supportedDataMapDefinitionFileExts, + ...supportedSchemaFileExts, + ]); + ext.context = context; ext.outputChannel = createAzExtOutputChannel('Azure Logic Apps (Standard)', ext.prefix); @@ -76,6 +89,7 @@ export async function activate(context: vscode.ExtensionContext) { export function deactivate(): Promise { stopDesignTimeApi(); + stopDataMapperBackend(); return undefined; } From b908e2d7bb7d16bb735d7e5cdd9ec9b771435cb9 Mon Sep 17 00:00:00 2001 From: Elaina-Lee <144840522+Elaina-Lee@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:27:41 -0700 Subject: [PATCH 03/11] feat(data mapper): Moved azureDataMapper commands to azureLogicAppsStandard in package.json (#3500) * moved azureDataMapper. commands in package.json from vs-dm to vs-designer * moved data mapper commands to constants * wiped out azureDataMapper telemetry for data mapper commands in vs-code-designer * fixed typos and wrongly referenced variable * added .dataMap in between the commands --- .../commands/dataMapper/DataMapperPanel.ts | 7 +-- .../src/app/commands/dataMapper/dataMapper.ts | 3 +- .../src/app/commands/registerCommands.ts | 4 +- apps/vs-code-designer/src/constants.ts | 10 +++- apps/vs-code-designer/src/package.json | 50 +++++++++++++++++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts index 49fada43418..6e7387bfb13 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts @@ -1,3 +1,4 @@ +import { extensionCommand } from '../../../constants'; import { ext } from '../../../extensionVariables'; import DataMapperExt from './DataMapperExt'; import { @@ -229,7 +230,7 @@ export default class DataMapperPanel { } public addSchemaFromFile(filePath: string, schemaType: SchemaType) { - callWithTelemetryAndErrorHandlingSync('azureDataMapper.addSchemaFromFile', (_context: IActionContext) => { + callWithTelemetryAndErrorHandlingSync(extensionCommand.dataMapAddSchemaFromFile, (_context: IActionContext) => { fs.readFile(filePath, 'utf8').then((text: string) => { const primarySchemaFileName = path.basename(filePath); // Ex: inpSchema.xsd const expectedPrimarySchemaPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), schemasPath, primarySchemaFileName); @@ -273,7 +274,7 @@ export default class DataMapperPanel { } public saveMapDefinition(mapDefinition: string) { - callWithTelemetryAndErrorHandlingSync('azureDataMapper.saveMapDefinition', (_context: IActionContext) => { + callWithTelemetryAndErrorHandlingSync(extensionCommand.dataMapSaveMapDefinition, (_context: IActionContext) => { // Delete *draft* map definition as it's no longer needed this.deleteDraftDataMapDefinition(); @@ -306,7 +307,7 @@ export default class DataMapperPanel { } public saveMapXslt(mapXslt: string) { - callWithTelemetryAndErrorHandlingSync('azureDataMapper.saveMapXslt', (_context: IActionContext) => { + callWithTelemetryAndErrorHandlingSync(extensionCommand.dataMapSaveMapXslt, (_context: IActionContext) => { const fileName = `${this.dataMapName}${mapXsltExtension}`; const dataMapFolderPath = path.join(DataMapperExt.getWorkspaceFolderFsPath(), dataMapsPath); const filePath = path.join(dataMapFolderPath, fileName); diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts b/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts index bc6e9fc26e9..42cc4d5eb00 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +import { extensionCommand } from '../../../constants'; import DataMapperExt from './DataMapperExt'; import { dataMapDefinitionsPath, draftMapDefinitionSuffix, schemasPath, supportedDataMapDefinitionFileExts } from './extensionConfig'; import type { MapDefinitionEntry } from '@microsoft/logic-apps-data-mapper'; @@ -99,7 +100,7 @@ export const loadDataMapFileCmd = async (context: IActionContext, uri: Uri) => { const attemptToResolveMissingSchemaFile = async (schemaName: string, schemaPath: string): Promise => { return !!(await callWithTelemetryAndErrorHandling( - 'azureDataMapper.attemptToResolveMissingSchemaFile', + extensionCommand.dataMapAttemptToResolveMissingSchemaFile, async (_context: IActionContext) => { const findSchemaFileButton = 'Find schema file'; const clickedButton = await window.showErrorMessage( diff --git a/apps/vs-code-designer/src/app/commands/registerCommands.ts b/apps/vs-code-designer/src/app/commands/registerCommands.ts index 0fa50998dcd..b6fe3c96388 100644 --- a/apps/vs-code-designer/src/app/commands/registerCommands.ts +++ b/apps/vs-code-designer/src/app/commands/registerCommands.ts @@ -128,6 +128,6 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping(extensionCommand.startRemoteDebug, startRemoteDebug); // Data Mapper Commands - registerCommand('azureDataMapper.createNewDataMap', (context: IActionContext) => createNewDataMapCmd(context)); - registerCommand('azureDataMapper.loadDataMapFile', (context: IActionContext, uri: Uri) => loadDataMapFileCmd(context, uri)); + registerCommand(extensionCommand.createNewDataMap, (context: IActionContext) => createNewDataMapCmd(context)); + registerCommand(extensionCommand.loadDataMapFile, (context: IActionContext, uri: Uri) => loadDataMapFileCmd(context, uri)); } diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index 10871e16bd5..33011e576c6 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -67,6 +67,7 @@ export enum extensionCommand { openFile = 'azureLogicAppsStandard.openFile', createNewProject = 'azureLogicAppsStandard.createNewProject', createNewCodeProject = 'azureLogicAppsStandard.createNewCodeProject', + createNewDataMap = 'azureLogicAppsStandard.dataMap.createNewDataMap', createCodeless = 'azureLogicAppsStandard.createCodeless', createLogicApp = 'azureLogicAppsStandard.createLogicApp', createLogicAppAdvanced = 'azureLogicAppsStandard.createLogicAppAdvanced', @@ -117,9 +118,14 @@ export enum extensionCommand { startRemoteDebug = 'azureLogicAppsStandard.startRemoteDebug', validateLogicAppProjects = 'azureLogicAppsStandard.validateFunctionProjects', reportIssue = 'azureLogicAppsStandard.reportIssue', - dataMapSetSupportedDataMapDefinitionFileExts = 'azureLogicAppsStandard.dataMap.setUpportedDataMapDefinitionFileExts', + loadDataMapFile = 'azureLogicAppsStandard.dataMap.loadDataMapFile', + dataMapAddSchemaFromFile = 'azureLogicAppsStandard.dataMap.addSchemaFromFile', + dataMapAttemptToResolveMissingSchemaFile = 'azureLogicAppsStandard.dataMap.attemptToResolveMissingSchemaFile', + dataMapSetSupportedDataMapDefinitionFileExts = 'azureLogicAppsStandard.dataMap.setSupportedDataMapDefinitionFileExts', dataMapSetSupportedSchemaFileExts = 'azureLogicAppsStandard.dataMap.setSupportedSchemaFileExts', - dataMapSetSupportedFileExts = 'azureLogicAppsStandard.dataMap.setSupportedSupportedFileExts', + dataMapSetSupportedFileExts = 'azureLogicAppsStandard.dataMap.setSupportedFileExts', + dataMapSaveMapDefinition = 'azureLogicAppsStandard.dataMap.saveMapDefinition', + dataMapSaveMapXslt = 'azureLogicAppsStandard.dataMap.saveMapXslt', } // Context diff --git a/apps/vs-code-designer/src/package.json b/apps/vs-code-designer/src/package.json index 8280879f136..1bf8ac5d135 100644 --- a/apps/vs-code-designer/src/package.json +++ b/apps/vs-code-designer/src/package.json @@ -305,6 +305,15 @@ "command": "azureLogicAppsStandard.reportIssue", "title": "Report Issue...", "category": "Azure Logic Apps" + }, + { + "command": "azureLogicAppsStandard.dataMap.createNewDataMap", + "title": "Data Mapper: Create new data map", + "icon": "$(add)" + }, + { + "command": "azureLogicAppsStandard.dataMap.loadDataMapFile", + "title": "Data Mapper: Load existing data map" } ], "submenus": [ @@ -315,6 +324,24 @@ "dark": "assets/logicapp.png" }, "label": "Azure Logic Apps" + }, + { + "id": "azureLogicAppsStandard.dataMapperMenu", + "label": "Data Mapper" + } + ], + "views": { + "azure": [ + { + "id": "azDataMapper", + "name": "Data Mapper" + } + ] + }, + "viewsWelcome": [ + { + "view": "azDataMapper", + "contents": "[Create new data map](command:azureLogicAppsStandard.dataMap.createNewDataMap)\n[Load existing data map](command:azureLogicAppsStandard.dataMap.loadDataMapFile)" } ], "menus": { @@ -346,6 +373,11 @@ "submenu": "azureLogicAppsStandard.submenus.workspaceActions", "when": "view == azureWorkspace", "group": "navigation@2" + }, + { + "command": "azureLogicAppsStandard.dataMap.createNewDataMap", + "when": "view == azDataMapper", + "group": "navigation@1" } ], "view/item/context": [ @@ -610,6 +642,11 @@ "command": "azureLogicAppsStandard.enableAzureConnectors", "when": "resourceFilename==workflow.json", "group": "navigation@2" + }, + { + "submenu": "azureLogicAppsStandard.dataMapperMenu", + "group": "navigation", + "when": "resourceExtname in azureLogicAppsStandard.dataMap.setSupportedFileExts" } ], "commandPalette": [ @@ -645,6 +682,13 @@ "command": "azureLogicAppsStandard.viewProperties", "when": "never" } + ], + "azureLogicAppsStandard.dataMapperMenu": [ + { + "command": "azureLogicAppsStandard.dataMap.loadDataMapFile", + "group": "navigation", + "when": "resourceExtname in azureLogicAppsStandard.dataMap.setSupportedDataMapDefinitionFileExts" + } ] }, "configuration": [ @@ -770,6 +814,11 @@ "type": "boolean", "description": "Show a warning when an Azure Functions .NET project was detected that has mismatched target frameworks.", "default": true + }, + "azureLogicAppsStandard.useExpandedFunctionCards": { + "type": "boolean", + "default": true, + "description": "Default the data mapper to use the expanded function cards." } } } @@ -828,6 +877,7 @@ "onCommand:azureLogicAppsStandard.startRemoteDebug", "onView:azureWorkspace", "onView:azureResourceGroups", + "onView:azDataMapper", "workspaceContains:host.json", "workspaceContains:*/host.json", "onDebugInitialConfigurations" From 282608c57116a5fb332a1d4c5ca3c1cb5fb30f05 Mon Sep 17 00:00:00 2001 From: Elaina-Lee <144840522+Elaina-Lee@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:50:37 -0700 Subject: [PATCH 04/11] feat(Data Mapper) Merged Readme files (#3508) merged readme files into vs-code-designer --- apps/vs-code-designer/src/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/vs-code-designer/src/README.md b/apps/vs-code-designer/src/README.md index 23a4067f310..82ad9f59548 100644 --- a/apps/vs-code-designer/src/README.md +++ b/apps/vs-code-designer/src/README.md @@ -4,6 +4,10 @@ In Visual Studio Code, you can use the Azure Logic Apps (Standard) extension to > Sign up today for your free Azure account and receive 12 months of free popular services, $200 free credit and 25+ always free services 👉 [Start Free](https://azure.microsoft.com/free/open-source). +# Azure Logic Apps - Data Mapper extension for Visual Studio Code + +In Visual Studio Code, you can graphically describe transformations by mapping relationships between data types in a source schema and a target schema. After you install the Azure Logic Apps - Data Mapper extension, you can create direct basic relationships and more complex transformations using functions, handling any translation between supported schema types in the backend. For more information, see [Create maps to transform data in Azure Logic Apps with Visual Studio Code (preview)](https://go.microsoft.com/fwlink/?linkid=2234193). + ## Azure Logic Apps (Standard) moves to the Resources tab 🎉 Version 2.15.15 and later: The Azure Logic Apps extension now follows the design pattern that Azure extensions follow. Previously, in the Azure window, an Azure Logic Apps extension section showed your Azure subscriptions and associated "remote" logic apps hosted in Azure. From 4c8c7ccce910875a8c57ad623ab5654549248f6c Mon Sep 17 00:00:00 2001 From: Elaina-Lee <144840522+Elaina-Lee@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:06:41 -0700 Subject: [PATCH 05/11] feat(Data Mapper): changed outputPaths to vs-code-designer/vs-code-data-mapper (#3513) * added tsconfig to fix merge undefined issue * initialized dataMapPanelManagers for uninitialized error * fixed all outputPath from dm/webview to designer/dm * switched order of build-react & renamed folder * fixed path in webviewHTML for dataMapperPanel & changed imports to fix tsConfig change errors * moved log, showWarning, showError to main extension * changed _setWeebviewHthml to setter using helper * localized all dataMapper log related statements --- apps/vs-code-data-mapper-react/project.json | 2 +- .../app/commands/dataMapper/DataMapperExt.ts | 18 +----- .../commands/dataMapper/DataMapperPanel.ts | 61 +++++++++---------- .../commands/dataMapper/FxWorkflowRuntime.ts | 8 +-- .../src/app/commands/dataMapper/dataMapper.ts | 8 ++- .../src/app/commands/pickFuncProcess.ts | 6 +- .../src/app/utils/workspace.ts | 3 +- .../src/extensionVariables.ts | 19 +++++- apps/vs-code-designer/tsconfig.json | 3 +- libs/data-mapper/project.json | 2 +- package.json | 2 +- 11 files changed, 69 insertions(+), 63 deletions(-) diff --git a/apps/vs-code-data-mapper-react/project.json b/apps/vs-code-data-mapper-react/project.json index 18cfa1bff00..f7864368366 100644 --- a/apps/vs-code-data-mapper-react/project.json +++ b/apps/vs-code-data-mapper-react/project.json @@ -10,7 +10,7 @@ "defaultConfiguration": "production", "options": { "compiler": "babel", - "outputPath": "dist/apps/vs-code-data-mapper/webview", + "outputPath": "dist/apps/vs-code-designer/vs-code-data-mapper", "index": "apps/vs-code-data-mapper-react/src/index.html", "baseHref": "/", "main": "apps/vs-code-data-mapper-react/src/main.tsx", diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts index e9d4b9a686f..06bfb06ccf4 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts @@ -1,4 +1,5 @@ import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; import DataMapperPanel from './DataMapperPanel'; import { startBackendRuntime } from './FxWorkflowRuntime'; import { webviewType } from './extensionConfig'; @@ -51,26 +52,11 @@ export default class DataMapperExt { // From here, VSIX will handle any other initial-load-time events once receive webviewLoaded msg } - public static log(text: string) { - ext.outputChannel.appendLine(text); - ext.outputChannel.show(); - } - - public static showWarning(errMsg: string) { - DataMapperExt.log(errMsg); - window.showWarningMessage(errMsg); - } - - public static showError(errMsg: string) { - DataMapperExt.log(errMsg); - window.showErrorMessage(errMsg); - } - public static getWorkspaceFolderFsPath() { if (workspace.workspaceFolders) { return workspace.workspaceFolders[0].uri.fsPath; } else { - DataMapperExt.showError('No VS Code folder/workspace found...'); + ext.showError(localize('MissingWorkspace', 'No VS Code folder/workspace found...')); return ''; } } diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts index 6e7387bfb13..f6fe745d4db 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/DataMapperPanel.ts @@ -1,5 +1,7 @@ import { extensionCommand } from '../../../constants'; import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; +import { getWebViewHTML } from '../../utils/codeless/getWebViewHTML'; import DataMapperExt from './DataMapperExt'; import { dataMapDefinitionsPath, @@ -26,7 +28,7 @@ import { } from 'fs'; import * as path from 'path'; import type { WebviewPanel } from 'vscode'; -import { RelativePattern, Uri, window, workspace } from 'vscode'; +import { RelativePattern, window, workspace } from 'vscode'; export default class DataMapperPanel { public panel: WebviewPanel; @@ -81,24 +83,7 @@ export default class DataMapperPanel { } private async _setWebviewHtml() { - // Get webview content, converting links to VS Code URIs - const indexPath = path.join(ext.context.extensionPath, '/webview/index.html'); - const html = await fs.readFile(indexPath, 'utf-8'); - // 1. Get all links prefixed by href or src - const matchLinks = /(href|src)="([^"]*)"/g; - // 2. Transform the result of the regex into a vscode's URI format - const toUri = (_: unknown, prefix: 'href' | 'src', link: string) => { - // For HTML elements - if (link === '#') { - return `${prefix}="${link}"`; - } - // For scripts & links - const pth = path.join(ext.context.extensionPath, '/webview/', link); - const uri = Uri.file(pth); - return `${prefix}="${this.panel.webview.asWebviewUri(uri)}"`; - }; - - this.panel.webview.html = html.replace(matchLinks, toUri); + this.panel.webview.html = await getWebViewHTML('vs-code-data-mapper', this.panel); } public sendMsgToWebview(msg: MessageToWebview) { @@ -117,7 +102,7 @@ export default class DataMapperPanel { break; case 'webviewRscLoadError': // Handle DM top-level errors (such as loading schemas added from file, or general function manifest fetching issues) - DataMapperExt.showError(`Error loading Data Mapper resource: ${msg.data}`); + ext.showError(localize('WebviewRscLoadError', `Error loading Data Mapper resource: "{0}"`, msg.data)); break; case 'addSchemaFromFile': { this.addSchemaFromFile(msg.data.path, msg.data.type); @@ -247,8 +232,14 @@ export default class DataMapperPanel { // Check that the schema file dependency exists in the same directory as the primary schema file if (!fileExistsSync(schemaFilePath)) { - DataMapperExt.showError( - `Schema loading error: couldn't find schema file dependency ${schemaFile} in the same directory as ${primarySchemaFileName}. ${primarySchemaFileName} will still be copied to the Schemas folder.` + ext.showError( + localize( + 'SchemaLoadingError', + `Schema loading error: couldn't find schema file dependency + "{0}" in the same directory as "{1}". "{1}" will still be copied to the Schemas folder.`, + schemaFile, + primarySchemaFileName + ) ); return; } @@ -296,14 +287,14 @@ export default class DataMapperPanel { }); }); }) - .catch(DataMapperExt.showError); + .catch(ext.showError); }); } public saveMapMetadata(mapMetadata: string) { const vscodeFolderPath = this.getMapMetadataPath(); - fs.writeFile(vscodeFolderPath, mapMetadata, 'utf8').catch(DataMapperExt.showError); + fs.writeFile(vscodeFolderPath, mapMetadata, 'utf8').catch(ext.showError); } public saveMapXslt(mapXslt: string) { @@ -327,7 +318,7 @@ export default class DataMapperPanel { this.checkAndSetXslt(); }); }) - .catch(DataMapperExt.showError); + .catch(ext.showError); }); } @@ -342,7 +333,7 @@ export default class DataMapperPanel { .then(() => { fs.writeFile(filePath, mapDefFileContents, 'utf8'); }) - .catch(DataMapperExt.showError); + .catch(ext.showError); } private readMapMetadataFile(): MapMetadata | undefined { @@ -353,14 +344,22 @@ export default class DataMapperPanel { const metadataJson = JSON.parse(fileBuffer.toString()) as MapMetadata; return metadataJson; } catch { - DataMapperExt.showError( - `Data map metadata file found at ${vscodeFolderPath} contains invalid JSON. Data map will load without metadata file.` + ext.showError( + localize( + 'MetadataInvalidJSON', + `Data map metadata file found at "{0}" contains invalid JSON. Data map will load without metadata file.`, + vscodeFolderPath + ) ); return undefined; } } else { - DataMapperExt.showWarning( - `Data map metadata not found at path ${vscodeFolderPath}. This file configures your function positioning and other info. Please save your map to regenerate the file.` + ext.showWarning( + localize( + 'MetadataNotFound', + `Data map metadata not found at path "{0}". This file configures your function positioning and other info. Please save your map to regenerate the file.`, + vscodeFolderPath + ) ); return undefined; } @@ -391,7 +390,7 @@ export default class DataMapperPanel { }); }); } else { - DataMapperExt.showWarning(`XSLT file not detected for ${this.dataMapName}`); + ext.showWarning(localize('XSLTFileNotDetected', `XSLT file not detected for "{0}"`, this.dataMapName)); } } diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts index f62f31bee66..fc6233ca60b 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts @@ -1,5 +1,5 @@ import { ext } from '../../../extensionVariables'; -import DataMapperExt from './DataMapperExt'; +import { localize } from '../../../localize'; import { backendRuntimeBaseUrl, backendRuntimeTimeout, @@ -40,7 +40,7 @@ export async function startBackendRuntime(projectPath: string): Promise { progress.report({ message: 'Starting backend runtime, this may take a few seconds...' }); if (await isBackendRuntimeUp(url)) { - DataMapperExt.log('Backend runtime is already running'); + ext.log(localize('RuntimeAlreadyRunning', 'Backend runtime is already running')); return; } @@ -63,7 +63,7 @@ export async function startBackendRuntime(projectPath: string): Promise { window.showErrorMessage('Backend runtime could not be started'); const errMsg = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error'; - DataMapperExt.log(`Backend runtime failed to start: ${errMsg}`); + ext.log(localize('RuntimeFailedToStart', `Backend runtime failed to start: "{0}"`, errMsg)); } }); } @@ -127,7 +127,7 @@ function startBackendRuntimeProcess(workingDirectory: string | undefined, comman shell: true, }; - DataMapperExt.log(`Running command: "${command} ${formattedArgs}"...`); + ext.log(localize('RunningCommand', `Running command: ""{0}" "{formattedArgs}""...`, command, formattedArgs)); ext.dataMapperChildProcess = cp.spawn(command, args, options); ext.dataMapperChildProcess.stdout?.on('data', (data: string | Buffer) => { diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts b/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts index 42cc4d5eb00..14cfdec022e 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/dataMapper.ts @@ -1,5 +1,7 @@ /* eslint-disable no-param-reassign */ import { extensionCommand } from '../../../constants'; +import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; import DataMapperExt from './DataMapperExt'; import { dataMapDefinitionsPath, draftMapDefinitionSuffix, schemasPath, supportedDataMapDefinitionFileExts } from './extensionConfig'; import type { MapDefinitionEntry } from '@microsoft/logic-apps-data-mapper'; @@ -89,7 +91,7 @@ export const loadDataMapFileCmd = async (context: IActionContext, uri: Uri) => { typeof mapDefinition.$targetSchema !== 'string' ) { context.telemetry.properties.eventDescription = 'Attempted to load invalid map, missing schema definitions'; - DataMapperExt.showError('Invalid map definition: $sourceSchema and $targetSchema must be defined.'); + ext.showError(localize('MissingSourceTargetSchema', 'Invalid map definition: $sourceSchema and $targetSchema must be defined.')); return; } @@ -143,7 +145,7 @@ export const loadDataMapFileCmd = async (context: IActionContext, uri: Uri) => { context.telemetry.properties.result = 'Canceled'; context.telemetry.properties.missingSourceSchema = 'true'; - DataMapperExt.showError('No source schema file was selected. Aborting load...'); + ext.showError(localize('MissingSourceSchema', 'No source schema file was selected. Aborting load...')); return; } } @@ -155,7 +157,7 @@ export const loadDataMapFileCmd = async (context: IActionContext, uri: Uri) => { context.telemetry.properties.result = 'Canceled'; context.telemetry.properties.missingTargetSchema = 'true'; - DataMapperExt.showError('No target schema file was selected. Aborting load...'); + ext.showError(localize('MissingTargetSchema', 'No target schema file was selected. Aborting load...')); return; } } diff --git a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts index 7593c98946b..6751e29122b 100644 --- a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts +++ b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts @@ -24,7 +24,9 @@ import { ProjectLanguage } from '@microsoft/vscode-extension'; import type { IPreDebugValidateResult, IProcessInfo } from '@microsoft/vscode-extension'; import * as unixPsTree from 'ps-tree'; import * as vscode from 'vscode'; -import * as parse from 'yargs-parser'; +import yargsPraser from 'yargs-parser'; + +//TODO: revisit this import again type OSAgnosticProcess = { command: string | undefined; pid: number | string }; type ActualUnixPS = unixPsTree.PS & { COMM?: string }; @@ -255,7 +257,7 @@ async function getWindowsChildren(pid: number): Promise { function getFunctionRuntimePort(funcTask: vscode.Task): number { const { command } = funcTask.definition; try { - const args = parse(command); + const args = yargsPraser(command); const port = args['port'] || args['p'] || undefined; return port ?? Number(defaultFuncPort); } catch { diff --git a/apps/vs-code-designer/src/app/utils/workspace.ts b/apps/vs-code-designer/src/app/utils/workspace.ts index 41672fc44e4..e89ee51f922 100644 --- a/apps/vs-code-designer/src/app/utils/workspace.ts +++ b/apps/vs-code-designer/src/app/utils/workspace.ts @@ -9,7 +9,8 @@ import { isPathEqual, isSubpath } from './fs'; import { isNullOrUndefined, isString } from '@microsoft/utils-logic-apps'; import { UserCancelledError } from '@microsoft/vscode-azext-utils'; import type { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; -import * as globby from 'globby'; +import globby from 'globby'; +//TODO: revisit this import again import * as path from 'path'; import * as vscode from 'vscode'; diff --git a/apps/vs-code-designer/src/extensionVariables.ts b/apps/vs-code-designer/src/extensionVariables.ts index 0115fe00954..6bf263009c8 100644 --- a/apps/vs-code-designer/src/extensionVariables.ts +++ b/apps/vs-code-designer/src/extensionVariables.ts @@ -9,7 +9,7 @@ import type { Site } from '@azure/arm-appservice'; import type { IAzExtOutputChannel } from '@microsoft/vscode-azext-utils'; import type { AzureHostExtensionApi } from '@microsoft/vscode-azext-utils/hostapi'; import type * as cp from 'child_process'; -import type { ExtensionContext, WebviewPanel } from 'vscode'; +import { window, type ExtensionContext, type WebviewPanel } from 'vscode'; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts @@ -39,7 +39,7 @@ export namespace ext { export let rgApi: AzureHostExtensionApi; // Data Mapper panel - export let dataMapPanelManagers: DataMapperPanelDictionary; + export const dataMapPanelManagers: DataMapperPanelDictionary = {}; // Functions export const funcCliPath: string = func; @@ -60,6 +60,21 @@ export namespace ext { [webViewKey.export]: {}, [webViewKey.overview]: {}, }; + + export const log = (text: string) => { + ext.outputChannel.appendLine(text); + ext.outputChannel.show(); + }; + + export const showWarning = (errMsg: string) => { + ext.log(errMsg); + window.showWarningMessage(errMsg); + }; + + export const showError = (errMsg: string) => { + ext.log(errMsg); + window.showErrorMessage(errMsg); + }; } export enum ExtensionCommand { diff --git a/apps/vs-code-designer/tsconfig.json b/apps/vs-code-designer/tsconfig.json index aec65d3ea30..0d3d78fe677 100644 --- a/apps/vs-code-designer/tsconfig.json +++ b/apps/vs-code-designer/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "jsx": "react-jsx", "allowSyntheticDefaultImports": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "esModuleInterop": true }, "files": [ "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", diff --git a/libs/data-mapper/project.json b/libs/data-mapper/project.json index 93f26488a70..ad217f5a3a2 100644 --- a/libs/data-mapper/project.json +++ b/libs/data-mapper/project.json @@ -27,7 +27,7 @@ }, "configurations": { "vscode": { - "outputPath": "dist/apps/vs-code-data-mapper/data-mapper" + "outputPath": "dist/apps/vs-code-designer/vs-code-data-mapper/data-mapper" } } }, diff --git a/package.json b/package.json index 473858fb6cb..858b88f56aa 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build:chatbot": "nx run chatbot:build", "build:parsers": "nx run parsers:build", "build:vscode-react": "nx run vs-code-react:build", - "build:vscode-designer": "nx run vs-code-designer:build && nx run vs-code-designer-react:build && nx run vs-code-react:build", + "build:vscode-designer": "nx run vs-code-designer:build && nx run vs-code-data-mapper-react:build && nx run vs-code-designer-react:build && nx run vs-code-react:build", "build:vscode-datamapper": "nx run vs-code-data-mapper:build && nx run vs-code-data-mapper-react:build", "build:utils": "nx run utils:build", "build:docs": "nx build logic-apps-ux-docs", From 3943b395c17236351e0e1ff3f0b04480bf9ac540 Mon Sep 17 00:00:00 2001 From: Elaina Lee Date: Wed, 25 Oct 2023 11:26:03 -0700 Subject: [PATCH 06/11] fixed typo from {formattedArgs} to {1} --- .../src/app/commands/dataMapper/FxWorkflowRuntime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts index fc6233ca60b..50a047bf14c 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts @@ -127,7 +127,7 @@ function startBackendRuntimeProcess(workingDirectory: string | undefined, comman shell: true, }; - ext.log(localize('RunningCommand', `Running command: ""{0}" "{formattedArgs}""...`, command, formattedArgs)); + ext.log(localize('RunningCommand', `Running command: ""{0}" "{1}""...`, command, formattedArgs)); ext.dataMapperChildProcess = cp.spawn(command, args, options); ext.dataMapperChildProcess.stdout?.on('data', (data: string | Buffer) => { From 3388a53d4f10fe2902cfb59c28ffb7fb4fe467b3 Mon Sep 17 00:00:00 2001 From: Elaina Lee Date: Wed, 25 Oct 2023 14:34:51 -0700 Subject: [PATCH 07/11] fixed typos and import that was giving warning --- apps/vs-code-designer/src/app/commands/pickFuncProcess.ts | 4 ++-- libs/data-mapper/src/lib/ui/ReactFlowWrapper.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts index 6751e29122b..b5ee229d22b 100644 --- a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts +++ b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts @@ -24,7 +24,7 @@ import { ProjectLanguage } from '@microsoft/vscode-extension'; import type { IPreDebugValidateResult, IProcessInfo } from '@microsoft/vscode-extension'; import * as unixPsTree from 'ps-tree'; import * as vscode from 'vscode'; -import yargsPraser from 'yargs-parser'; +import yargsParser from 'yargs-parser'; //TODO: revisit this import again @@ -257,7 +257,7 @@ async function getWindowsChildren(pid: number): Promise { function getFunctionRuntimePort(funcTask: vscode.Task): number { const { command } = funcTask.definition; try { - const args = yargsPraser(command); + const args = yargsParser(command); const port = args['port'] || args['p'] || undefined; return port ?? Number(defaultFuncPort); } catch { diff --git a/libs/data-mapper/src/lib/ui/ReactFlowWrapper.tsx b/libs/data-mapper/src/lib/ui/ReactFlowWrapper.tsx index 1c8540394c0..9e47d592f04 100644 --- a/libs/data-mapper/src/lib/ui/ReactFlowWrapper.tsx +++ b/libs/data-mapper/src/lib/ui/ReactFlowWrapper.tsx @@ -47,7 +47,7 @@ import type { Edge as ReactFlowEdge, Node as ReactFlowNode, } from 'reactflow'; -import ReactFlow, { ConnectionLineType, useKeyPress, useNodesState } from 'reactflow'; +import { ReactFlow, ConnectionLineType, useKeyPress, useNodesState } from 'reactflow'; import { ActionCreators } from 'redux-undo'; type CanvasExtent = [[number, number], [number, number]]; From f01a46a881896bc305e730252c669fda661ff7f7 Mon Sep 17 00:00:00 2001 From: Elaina Lee Date: Wed, 25 Oct 2023 14:36:56 -0700 Subject: [PATCH 08/11] repositioned TODO to more readable area --- apps/vs-code-designer/src/app/commands/pickFuncProcess.ts | 3 +-- apps/vs-code-designer/src/app/utils/workspace.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts index b5ee229d22b..478227a4eff 100644 --- a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts +++ b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts @@ -24,10 +24,9 @@ import { ProjectLanguage } from '@microsoft/vscode-extension'; import type { IPreDebugValidateResult, IProcessInfo } from '@microsoft/vscode-extension'; import * as unixPsTree from 'ps-tree'; import * as vscode from 'vscode'; +//TODO: revisit this import again (yargsParser) import yargsParser from 'yargs-parser'; -//TODO: revisit this import again - type OSAgnosticProcess = { command: string | undefined; pid: number | string }; type ActualUnixPS = unixPsTree.PS & { COMM?: string }; diff --git a/apps/vs-code-designer/src/app/utils/workspace.ts b/apps/vs-code-designer/src/app/utils/workspace.ts index e89ee51f922..3d5f9eb4e99 100644 --- a/apps/vs-code-designer/src/app/utils/workspace.ts +++ b/apps/vs-code-designer/src/app/utils/workspace.ts @@ -9,8 +9,8 @@ import { isPathEqual, isSubpath } from './fs'; import { isNullOrUndefined, isString } from '@microsoft/utils-logic-apps'; import { UserCancelledError } from '@microsoft/vscode-azext-utils'; import type { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; +//TODO: revisit this import again (globby) import globby from 'globby'; -//TODO: revisit this import again import * as path from 'path'; import * as vscode from 'vscode'; From 8cc5f99222dfab6a4dae8656783eccb93c8a5fe8 Mon Sep 17 00:00:00 2001 From: Elaina Lee Date: Wed, 25 Oct 2023 15:15:07 -0700 Subject: [PATCH 09/11] removed data mapper extension part in readme & removed duplicate/unused extensionConfig --- apps/vs-code-designer/src/README.md | 4 ---- .../src/app/commands/dataMapper/FxWorkflowRuntime.ts | 2 +- .../src/app/commands/dataMapper/extensionConfig.ts | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/vs-code-designer/src/README.md b/apps/vs-code-designer/src/README.md index 82ad9f59548..23a4067f310 100644 --- a/apps/vs-code-designer/src/README.md +++ b/apps/vs-code-designer/src/README.md @@ -4,10 +4,6 @@ In Visual Studio Code, you can use the Azure Logic Apps (Standard) extension to > Sign up today for your free Azure account and receive 12 months of free popular services, $200 free credit and 25+ always free services 👉 [Start Free](https://azure.microsoft.com/free/open-source). -# Azure Logic Apps - Data Mapper extension for Visual Studio Code - -In Visual Studio Code, you can graphically describe transformations by mapping relationships between data types in a source schema and a target schema. After you install the Azure Logic Apps - Data Mapper extension, you can create direct basic relationships and more complex transformations using functions, handling any translation between supported schema types in the backend. For more information, see [Create maps to transform data in Azure Logic Apps with Visual Studio Code (preview)](https://go.microsoft.com/fwlink/?linkid=2234193). - ## Azure Logic Apps (Standard) moves to the Resources tab 🎉 Version 2.15.15 and later: The Azure Logic Apps extension now follows the design pattern that Azure extensions follow. Previously, in the Azure window, an Azure Logic Apps extension section showed your Azure subscriptions and associated "remote" logic apps hosted in Azure. diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts index 50a047bf14c..78716d1a6da 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts @@ -1,10 +1,10 @@ +import { hostFileName } from '../../../constants'; import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; import { backendRuntimeBaseUrl, backendRuntimeTimeout, hostFileContent, - hostFileName, settingsFileContent, settingsFileName, workflowDesignTimeDir, diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts b/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts index f9728d73b89..4ea0a3df019 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts @@ -1,6 +1,4 @@ export const webviewType = 'dataMapperWebview'; -export const outputChannelTitle = 'Data Mapper'; -export const outputChannelPrefix = 'azureLogicAppsDataMapper'; export const supportedDataMapDefinitionFileExts = ['.yml']; export const supportedSchemaFileExts = ['.xsd', '.json']; @@ -18,7 +16,6 @@ export const draftMapDefinitionSuffix = '.draft'; export const mapDefinitionExtension = '.yml'; export const mapXsltExtension = '.xslt'; -export const hostFileName = 'host.json'; export const settingsFileName = 'local.settings.json'; export const hostFileContent = { version: '2.0', From e09627cd4248fff4f4e5d62bbb342415cd60c74e Mon Sep 17 00:00:00 2001 From: Elaina Lee Date: Wed, 25 Oct 2023 15:47:02 -0700 Subject: [PATCH 10/11] moved shared constants to constants file --- .../commands/dataMapper/FxWorkflowRuntime.ts | 18 +++++------------- .../commands/dataMapper/extensionConfig.ts | 19 +------------------ apps/vs-code-designer/src/constants.ts | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts index 78716d1a6da..8c2809df6df 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/FxWorkflowRuntime.ts @@ -1,15 +1,7 @@ -import { hostFileName } from '../../../constants'; +import { designerStartApi, hostFileContent, hostFileName, localSettingsFileName, workflowDesignTimeDir } from '../../../constants'; import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; -import { - backendRuntimeBaseUrl, - backendRuntimeTimeout, - hostFileContent, - settingsFileContent, - settingsFileName, - workflowDesignTimeDir, - workflowMgmtApi, -} from './extensionConfig'; +import { backendRuntimeBaseUrl, dataMapLoadTimeout, settingsFileContent } from './extensionConfig'; import * as cp from 'child_process'; import { promises as fs, existsSync as fileExists } from 'fs'; import merge from 'lodash.merge'; @@ -34,7 +26,7 @@ export async function startBackendRuntime(projectPath: string): Promise { } // Note: Must append operationGroups as it's a valid endpoint to ping - const url = `${backendRuntimeBaseUrl}${ext.dataMapperRuntimePort}${workflowMgmtApi}operationGroups`; + const url = `${backendRuntimeBaseUrl}${ext.dataMapperRuntimePort}${designerStartApi}`; await window.withProgress({ location: ProgressLocation.Notification }, async (progress) => { progress.report({ message: 'Starting backend runtime, this may take a few seconds...' }); @@ -51,7 +43,7 @@ export async function startBackendRuntime(projectPath: string): Promise { if (runtimeWorkingDir) { await createDesignTimeDirectory(runtimeWorkingDir); await createJsonFile(runtimeWorkingDir, hostFileName, hostFileContent); - await createJsonFile(runtimeWorkingDir, settingsFileName, modifiedSettingsFileContent); + await createJsonFile(runtimeWorkingDir, localSettingsFileName, modifiedSettingsFileContent); startBackendRuntimeProcess(runtimeWorkingDir, 'func', 'host', 'start', '--port', `${ext.dataMapperRuntimePort}`); @@ -96,7 +88,7 @@ async function createJsonFile( } async function waitForBackendRuntimeStartUp(url: string, initialTime: number): Promise { - while (!(await isBackendRuntimeUp(url)) && new Date().getTime() - initialTime < backendRuntimeTimeout) { + while (!(await isBackendRuntimeUp(url)) && new Date().getTime() - initialTime < dataMapLoadTimeout) { await delay(1000); // Re-poll every X ms } diff --git a/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts b/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts index 4ea0a3df019..3fe90c7de79 100644 --- a/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts +++ b/apps/vs-code-designer/src/app/commands/dataMapper/extensionConfig.ts @@ -9,28 +9,12 @@ export const schemasPath = `${artifactsPath}/Schemas`; export const customXsltPath = `${artifactsPath}/DataMapper/Extensions/InlineXslt`; export const dataMapsPath = `${artifactsPath}/Maps`; export const dataMapDefinitionsPath = `${artifactsPath}/MapDefinitions`; -export const workflowDesignTimeDir = '/workflow-designtime'; export const defaultDataMapFilename = 'default'; export const draftMapDefinitionSuffix = '.draft'; export const mapDefinitionExtension = '.yml'; export const mapXsltExtension = '.xslt'; -export const settingsFileName = 'local.settings.json'; -export const hostFileContent = { - version: '2.0', - extensionBundle: { - id: 'Microsoft.Azure.Functions.ExtensionBundle.Workflows', - version: '[1.*, 2.0.0)', - }, - extensions: { - workflow: { - settings: { - 'Runtime.WorkflowOperationDiscoveryHostMode': 'true', - }, - }, - }, -}; export const settingsFileContent = { IsEncrypted: false, Values: { @@ -41,5 +25,4 @@ export const settingsFileContent = { }; export const backendRuntimeBaseUrl = 'http://localhost:'; -export const workflowMgmtApi = '/runtime/webhooks/workflow/api/management/'; -export const backendRuntimeTimeout = 60000; +export const dataMapLoadTimeout = 60000; diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index 4d19eabbee2..63a0d4c33a0 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -18,6 +18,9 @@ export const vscodeFolderName = '.vscode'; export const workflowFileName = 'workflow.json'; export const funcIgnoreFileName = '.funcignore'; +// Folder names +export const workflowDesignTimeDir = '/workflow-designtime'; + export const logicAppsStandardExtensionId = 'ms-azuretools.vscode-azurelogicapps'; // Functions @@ -172,6 +175,20 @@ export const localEmulatorConnectionString = 'UseDevelopmentStorage=true'; // host.json export const extensionBundleId = 'Microsoft.Azure.Functions.ExtensionBundle.Workflows'; +export const hostFileContent = { + version: '2.0', + extensionBundle: { + id: extensionBundleId, + version: defaultVersionRange, + }, + extensions: { + workflow: { + settings: { + 'Runtime.WorkflowOperationDiscoveryHostMode': 'true', + }, + }, + }, +}; // .NET export enum DotnetVersion { From 860c1a4f2d3c40aa6de034c8dae5dfd31485daef Mon Sep 17 00:00:00 2001 From: Elaina Lee Date: Wed, 25 Oct 2023 16:00:44 -0700 Subject: [PATCH 11/11] brought back data mapper readme in designer with subcontext --- apps/vs-code-designer/src/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/vs-code-designer/src/README.md b/apps/vs-code-designer/src/README.md index 23a4067f310..cf3614ede75 100644 --- a/apps/vs-code-designer/src/README.md +++ b/apps/vs-code-designer/src/README.md @@ -4,6 +4,10 @@ In Visual Studio Code, you can use the Azure Logic Apps (Standard) extension to > Sign up today for your free Azure account and receive 12 months of free popular services, $200 free credit and 25+ always free services 👉 [Start Free](https://azure.microsoft.com/free/open-source). +## Azure Logic Apps - Data Mapper for Visual Studio Code + +In Visual Studio Code, you can graphically describe transformations by mapping relationships between data types in a source schema and a target schema. After you install the Azure Logic Apps - Data Mapper, you can create direct basic relationships and more complex transformations using functions, handling any translation between supported schema types in the backend. For more information, see [Create maps to transform data in Azure Logic Apps with Visual Studio Code (preview)](https://go.microsoft.com/fwlink/?linkid=2234193). + ## Azure Logic Apps (Standard) moves to the Resources tab 🎉 Version 2.15.15 and later: The Azure Logic Apps extension now follows the design pattern that Azure extensions follow. Previously, in the Azure window, an Azure Logic Apps extension section showed your Azure subscriptions and associated "remote" logic apps hosted in Azure.