From 0c54e48a5560e27f5a6067792b1a8bac9fff3bf3 Mon Sep 17 00:00:00 2001 From: Ashwani Kumar Date: Fri, 25 Oct 2024 04:05:07 +0530 Subject: [PATCH] Added site runtime preview code behind ECS Config --- l10n/bundle.l10n.json | 2 +- src/client/extension.ts | 85 ++++++++++++++---- .../runtimeSitePreview/LaunchJsonHelper.ts | 88 +++++++++++++++++++ src/client/runtimeSitePreview/PreviewSite.ts | 74 ++++++++++++++++ src/common/ecs-features/ecsFeatureGates.ts | 10 +++ src/common/services/PPAPIService.ts | 31 +++++++ .../utilities/WorkspaceInfoFinderUtil.ts | 24 +++++ 7 files changed, 296 insertions(+), 18 deletions(-) create mode 100644 src/client/runtimeSitePreview/LaunchJsonHelper.ts create mode 100644 src/client/runtimeSitePreview/PreviewSite.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index ef2a7b412..35cb416d0 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -256,4 +256,4 @@ "The {0} represents profile's Azure Cloud Instances" ] } -} +} \ No newline at end of file diff --git a/src/client/extension.ts b/src/client/extension.ts index 2f023c292..aff96beee 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -40,12 +40,16 @@ import { ActiveOrgOutput } from "./pac/PacTypes"; import { desktopTelemetryEventNames } from "../common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames"; import { ArtemisService } from "../common/services/ArtemisService"; import { workspaceContainsPortalConfigFolder } from "../common/utilities/PathFinderUtil"; -import { getPortalsOrgURLs } from "../common/utilities/WorkspaceInfoFinderUtil"; +import { getPortalsOrgURLs, getWebsiteRecordID } from "../common/utilities/WorkspaceInfoFinderUtil"; import { EXTENSION_ID, SUCCESS } from "../common/constants"; import { AadIdKey, EnvIdKey, TenantIdKey } from "../common/OneDSLoggerTelemetry/telemetryConstants"; import { PowerPagesAppName, PowerPagesClientName } from "../common/ecs-features/constants"; import { ECSFeaturesClient } from "../common/ecs-features/ecsFeatureClient"; import { getECSOrgLocationValue } from "../common/utilities/Utils"; +import { ServiceEndpointCategory } from "../common/services/Constants"; +import { PPAPIService } from "../common/services/PPAPIService"; +import { EnableSiteRuntimePreview } from "../common/ecs-features/ecsFeatureGates"; +import { PreviewSite } from "./runtimeSitePreview/PreviewSite"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -101,22 +105,6 @@ export async function activate( ); } - // portal web view panel - _context.subscriptions.push( - vscode.commands.registerCommand( - "microsoft-powerapps-portals.preview-show", - () => { - _telemetry.sendTelemetryEvent("StartCommand", { - commandId: "microsoft-powerapps-portals.preview-show", - }); - oneDSLoggerWrapper.getLogger().traceInfo("StartCommand", { - commandId: "microsoft-powerapps-portals.preview-show" - }); - PortalWebView.createOrShow(); - } - ) - ); - // registering bootstrapdiff command _context.subscriptions.push( vscode.commands.registerCommand('microsoft-powerapps-portals.bootstrap-diff', async () => { @@ -195,6 +183,8 @@ export async function activate( ) || []; + let websiteURL = ""; + _context.subscriptions.push( orgChangeEvent(async (orgDetails: ActiveOrgOutput) => { const orgID = orgDetails.OrgId; @@ -249,6 +239,9 @@ export async function activate( copilotNotificationShown = true; } + if(artemisResponse!==null && isSiteRuntimePreviewEnabled()) { + websiteURL = await getWebSiteURL(workspaceFolders, artemisResponse?.stamp, orgDetails.EnvironmentId, _telemetry); + } }) ); @@ -270,6 +263,47 @@ export async function activate( vscode.commands.executeCommand('setContext', 'powerpages.websiteYmlExists', false); } + const registerPreviewShowCommand = async () => { + const isEnabled = isSiteRuntimePreviewEnabled(); + + _telemetry.sendTelemetryEvent("EnableSiteRuntimePreview", { + isEnabled: isEnabled.toString(), + websiteURL: websiteURL + }); + oneDSLoggerWrapper.getLogger().traceInfo("EnableSiteRuntimePreview", { + isEnabled: isEnabled.toString(), + websiteURL: websiteURL + }); + + if (!isEnabled || websiteURL === "") { + // portal web view panel + _context.subscriptions.push( + vscode.commands.registerCommand( + "microsoft-powerapps-portals.preview-show", + () => { + _telemetry.sendTelemetryEvent("StartCommand", { + commandId: "microsoft-powerapps-portals.preview-show", + }); + oneDSLoggerWrapper.getLogger().traceInfo("StartCommand", { + commandId: "microsoft-powerapps-portals.preview-show" + }); + PortalWebView.createOrShow(); + } + ) + ); + } else { + _context.subscriptions.push( + vscode.commands.registerCommand( + "microsoft-powerapps-portals.preview-show", + () => PreviewSite.launchBrowserAndDevToolsWithinVsCode(websiteURL) + ) + ); + } + }; + + await registerPreviewShowCommand(); + + const workspaceFolderWatcher = vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFolderChange); _context.subscriptions.push(workspaceFolderWatcher); @@ -299,6 +333,23 @@ export async function deactivate(): Promise { disposeNotificationPanel(); } +async function getWebSiteURL(workspaceFolders: WorkspaceFolder[], stamp: ServiceEndpointCategory, envId: string, telemetry: ITelemetry): Promise { + + const websiteRecordId = getWebsiteRecordID(workspaceFolders, telemetry); + const websiteDetails = await PPAPIService.getWebsiteDetailsByWebsiteRecordId(stamp, envId, websiteRecordId, _telemetry); + return websiteDetails?.websiteUrl || ""; +} + +function isSiteRuntimePreviewEnabled() { + const enableSiteRuntimePreview = ECSFeaturesClient.getConfig(EnableSiteRuntimePreview).enableSiteRuntimePreview + + if(enableSiteRuntimePreview === undefined) { + return false; + } + + return enableSiteRuntimePreview; +} + function didOpenTextDocument(document: vscode.TextDocument): void { // The debug options for the server // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging diff --git a/src/client/runtimeSitePreview/LaunchJsonHelper.ts b/src/client/runtimeSitePreview/LaunchJsonHelper.ts new file mode 100644 index 000000000..0ee52f41a --- /dev/null +++ b/src/client/runtimeSitePreview/LaunchJsonHelper.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from 'vscode'; + +export async function updateLaunchJsonConfig(url: string): Promise { + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + vscode.window.showErrorMessage('No workspace folder is open.'); + return; + } + + const launchJsonPath = vscode.Uri.joinPath(workspaceFolders[0].uri, '.vscode', 'launch.json'); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let launchJson: any; + let launchJsonDoc: vscode.TextDocument | undefined; + + try { + launchJsonDoc = await vscode.workspace.openTextDocument(launchJsonPath); + const launchJsonText = launchJsonDoc.getText(); + launchJson = launchJsonText ? JSON.parse(launchJsonText) : { configurations: [], compounds: [] }; + } catch (error) { + // If the file does not exist or is empty, initialize it + launchJson = { configurations: [], compounds: [] }; + } + + // Update or add the configurations for Microsoft Edge + const edgeConfigurations = [ + { + type: 'pwa-msedge', + name: 'Launch Microsoft Edge', + request: 'launch', + runtimeArgs: ['--remote-debugging-port=9222'], + url: url, + presentation: { + hidden: true + } + }, + { + type: 'pwa-msedge', + name: 'Launch Microsoft Edge in headless mode', + request: 'launch', + runtimeArgs: ['--headless', '--remote-debugging-port=9222'], + url: url, + presentation: { + hidden: true + } + }, + { + type: 'vscode-edge-devtools.debug', + name: 'Open Edge DevTools', + request: 'attach', + url: url, + presentation: { + hidden: true + } + } + ]; + + // Update or add the compounds for Microsoft Edge + const edgeCompounds = [ + { + name: 'Launch Edge Headless and attach DevTools', + configurations: ['Launch Microsoft Edge in headless mode', 'Open Edge DevTools'] + }, + { + name: 'Launch Edge and attach DevTools', + configurations: ['Launch Microsoft Edge', 'Open Edge DevTools'] + } + ]; + + // Merge the new configurations and compounds with the existing ones + launchJson.configurations = [...launchJson.configurations, ...edgeConfigurations]; + launchJson.compounds = [...launchJson.compounds, ...edgeCompounds]; + + // Write the updated launch.json file + const launchJsonContent = JSON.stringify(launchJson, null, 4); + await vscode.workspace.fs.writeFile(launchJsonPath, Buffer.from(launchJsonContent, 'utf8')); + } catch (e) { + if(e instanceof Error) { + vscode.window.showErrorMessage(`Failed to update launch.json: ${e.message}`); + } + } +} diff --git a/src/client/runtimeSitePreview/PreviewSite.ts b/src/client/runtimeSitePreview/PreviewSite.ts new file mode 100644 index 000000000..eeac06ab2 --- /dev/null +++ b/src/client/runtimeSitePreview/PreviewSite.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { updateLaunchJsonConfig } from './LaunchJsonHelper'; + +export class PreviewSite { + + static async launchBrowserAndDevToolsWithinVsCode(webSitePreviewURL: string): Promise { + + const edgeToolsExtensionId = 'ms-edgedevtools.vscode-edge-devtools'; + const edgeToolsExtension = vscode.extensions.getExtension(edgeToolsExtensionId); + + if (edgeToolsExtension) { + // Preserve the original state of the launch.json file and .vscode folder + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const vscodeFolderPath = workspaceFolder ? path.join(workspaceFolder.uri.fsPath, '.vscode') : null; + const launchJsonPath = vscodeFolderPath ? path.join(vscodeFolderPath, 'launch.json') : null; + let originalLaunchJsonContent: string | null = null; + let vscodeFolderExisted = false; + + if (vscodeFolderPath && fs.existsSync(vscodeFolderPath)) { + vscodeFolderExisted = true; + if (launchJsonPath && fs.existsSync(launchJsonPath)) { + originalLaunchJsonContent = fs.readFileSync(launchJsonPath, 'utf8'); + } + } + + await updateLaunchJsonConfig(webSitePreviewURL); + + try { + // Added a 2-second delay before executing the launchProject command to handle the case where the launch.json file is not saved yet + await new Promise(resolve => setTimeout(resolve, 2000)); + await vscode.commands.executeCommand('vscode-edge-devtools-view.launchProject'); + + } finally { + // Revert the changes made to the launch.json file and remove the .vscode folder if it was created + + // Added a 2-second delay to ensure that debug session is closed and then launch.json file is removed + await new Promise(resolve => setTimeout(resolve, 2000)); + if (launchJsonPath) { + if (originalLaunchJsonContent !== null) { + fs.writeFileSync(launchJsonPath, originalLaunchJsonContent, 'utf8'); + } else if (fs.existsSync(launchJsonPath)) { + fs.unlinkSync(launchJsonPath); + } + } + + if (vscodeFolderPath && !vscodeFolderExisted && fs.existsSync(vscodeFolderPath)) { + const files = fs.readdirSync(vscodeFolderPath); + if (files.length === 0) { + fs.rmdirSync(vscodeFolderPath); + } + } + } + } else { + const install = await vscode.window.showWarningMessage( + `The extension "${edgeToolsExtensionId}" is required to run this command. Do you want to install it now?`, + 'Install', 'Cancel' + ); + + if (install === 'Install') { + // Open the Extensions view with the specific extension + vscode.commands.executeCommand('workbench.extensions.search', edgeToolsExtensionId); + } + + return; + } + } +} diff --git a/src/common/ecs-features/ecsFeatureGates.ts b/src/common/ecs-features/ecsFeatureGates.ts index b49fe6453..7cacacbf3 100644 --- a/src/common/ecs-features/ecsFeatureGates.ts +++ b/src/common/ecs-features/ecsFeatureGates.ts @@ -48,3 +48,13 @@ export const { enablePowerpagesInGithubCopilot: false, }, }); + +export const { + feature: EnableSiteRuntimePreview +} = getFeatureConfigs({ + teamName: PowerPagesClientName, + description: 'Enable Site Runtime Preview in VS Code Desktop', + fallback: { + enableSiteRuntimePreview: false, + }, +}); diff --git a/src/common/services/PPAPIService.ts b/src/common/services/PPAPIService.ts index 1b9f959a3..bdbd88fa6 100644 --- a/src/common/services/PPAPIService.ts +++ b/src/common/services/PPAPIService.ts @@ -33,6 +33,37 @@ export class PPAPIService { return null; } + public static async getWebsiteDetailsByWebsiteRecordId(serviceEndpointStamp: ServiceEndpointCategory, environmentId: string, websiteRecordId: string, telemetry: ITelemetry): Promise { + + const websiteDetailsArray = await PPAPIService.getWebsiteDetails(serviceEndpointStamp, environmentId, telemetry); + const websiteDetails = websiteDetailsArray?.find((website) => website.websiteRecordId === websiteRecordId); + + if (websiteDetails) { + sendTelemetryEvent(telemetry, { eventName: VSCODE_EXTENSION_PPAPI_GET_WEBSITE_BY_ID_COMPLETED, orgUrl: websiteDetails.dataverseInstanceUrl }); + return websiteDetails; + } + return null; + } + + static async getWebsiteDetails(serviceEndpointStamp: ServiceEndpointCategory, environmentId: string, telemetry: ITelemetry): Promise { + try { + const accessToken = await powerPlatformAPIAuthentication(telemetry, true); + const response = await fetch(await PPAPIService.getPPAPIServiceEndpoint(serviceEndpointStamp, telemetry, environmentId), { + method: 'GET', + headers: getCommonHeaders(accessToken) + }); + + if (response.ok) { + const websiteDetailsArray = await response.json() as unknown as IWebsiteDetails[]; + return websiteDetailsArray; + } + } + catch (error) { + sendTelemetryEvent(telemetry, { eventName: VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_FAILED, errorMsg: (error as Error).message }); + } + return null; + } + static async getPPAPIServiceEndpoint(serviceEndpointStamp: ServiceEndpointCategory, telemetry: ITelemetry, environmentId: string, websitePreviewId?: string): Promise { let ppapiEndpoint = ""; diff --git a/src/common/utilities/WorkspaceInfoFinderUtil.ts b/src/common/utilities/WorkspaceInfoFinderUtil.ts index 2b4afc175..ec8256260 100644 --- a/src/common/utilities/WorkspaceInfoFinderUtil.ts +++ b/src/common/utilities/WorkspaceInfoFinderUtil.ts @@ -7,6 +7,9 @@ import { WorkspaceFolder } from 'vscode-languageserver/node'; import { glob } from 'glob'; +import * as path from 'path'; +import * as fs from 'fs'; +import { parse } from 'yaml'; import { ITelemetry } from '../OneDSLoggerTelemetry/telemetry/ITelemetry'; import { sendTelemetryEvent } from '../OneDSLoggerTelemetry/telemetry/telemetry'; @@ -36,3 +39,24 @@ export function getPortalsOrgURLs(workspaceRootFolders: WorkspaceFolder[] | null } return output; } + +export function getWebsiteRecordID(workspaceFolders: { uri: string }[], telemetry: ITelemetry): string { + try { + if (!workspaceFolders || workspaceFolders.length === 0) { + return ""; + } + + const workspaceRootFolder = workspaceFolders[0]; + const websiteYmlPath = path.join(workspaceRootFolder.uri, 'website.yml'); + if (fs.existsSync(websiteYmlPath)) { + const fileContent = fs.readFileSync(websiteYmlPath, 'utf8'); + const parsedYaml = parse(fileContent); + if (parsedYaml && parsedYaml.adx_websiteid) { + return parsedYaml.adx_websiteid; + } + } + } catch (exception) { + sendTelemetryEvent(telemetry, { methodName: getWebsiteRecordID.name, eventName: 'getWebsiteRecordID', exception: exception as Error }); + } + return ""; +}