Skip to content

Commit

Permalink
Added site runtime preview code behind ECS Config
Browse files Browse the repository at this point in the history
  • Loading branch information
sparrow1303 committed Oct 24, 2024
1 parent 6e9e863 commit 0c54e48
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 18 deletions.
2 changes: 1 addition & 1 deletion l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,4 @@
"The {0} represents profile's Azure Cloud Instances"
]
}
}
}
85 changes: 68 additions & 17 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -195,6 +183,8 @@ export async function activate(
) || [];


let websiteURL = "";

_context.subscriptions.push(
orgChangeEvent(async (orgDetails: ActiveOrgOutput) => {
const orgID = orgDetails.OrgId;
Expand Down Expand Up @@ -249,6 +239,9 @@ export async function activate(
copilotNotificationShown = true;

}
if(artemisResponse!==null && isSiteRuntimePreviewEnabled()) {
websiteURL = await getWebSiteURL(workspaceFolders, artemisResponse?.stamp, orgDetails.EnvironmentId, _telemetry);
}

})
);
Expand All @@ -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);

Expand Down Expand Up @@ -299,6 +333,23 @@ export async function deactivate(): Promise<void> {
disposeNotificationPanel();
}

async function getWebSiteURL(workspaceFolders: WorkspaceFolder[], stamp: ServiceEndpointCategory, envId: string, telemetry: ITelemetry): Promise<string> {

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
Expand Down
88 changes: 88 additions & 0 deletions src/client/runtimeSitePreview/LaunchJsonHelper.ts
Original file line number Diff line number Diff line change
@@ -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<void> {

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}`);
}
}
}
74 changes: 74 additions & 0 deletions src/client/runtimeSitePreview/PreviewSite.ts
Original file line number Diff line number Diff line change
@@ -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<void> {

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;
}
}
}
10 changes: 10 additions & 0 deletions src/common/ecs-features/ecsFeatureGates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
31 changes: 31 additions & 0 deletions src/common/services/PPAPIService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,37 @@ export class PPAPIService {
return null;
}

public static async getWebsiteDetailsByWebsiteRecordId(serviceEndpointStamp: ServiceEndpointCategory, environmentId: string, websiteRecordId: string, telemetry: ITelemetry): Promise<IWebsiteDetails | null> {

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<IWebsiteDetails[] | null> {
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<string> {

let ppapiEndpoint = "";
Expand Down
24 changes: 24 additions & 0 deletions src/common/utilities/WorkspaceInfoFinderUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 "";
}

0 comments on commit 0c54e48

Please sign in to comment.