diff --git a/media/runtime.css b/media/runtime.css new file mode 100644 index 000000000..9fdfcb4d2 --- /dev/null +++ b/media/runtime.css @@ -0,0 +1,227 @@ +:root { + --container-paddding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; +} + +html, +body { + height: 100%; + min-height: 100%; + padding: 0; + margin: 0; +} + +ol, +ul { + padding-left: var(--container-paddding); +} + +body > *, +form > * { + margin-block-start: var(--input-margin-vertical); + margin-block-end: var(--input-margin-vertical); +} + +*:focus { + outline-color: var(--vscode-focusBorder) !important; +} + +a { + color: var(--vscode-textLink-foreground); +} + +a:hover, +a:active { + color: var(--vscode-textLink-activeForeground); +} + +code { + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); +} +iframe { + width: 100%; + height: 100%; + border: none; + background: white; /* Browsers default to a white background */ +} + +.controls, +.find { + display: block; + width: 100%; + height: 2em; + margin-top: 0; + background: var(--vscode-editor-background); +} + +.controls button i { + display: flex; + margin-right: 0.3em; +} + +.displayContents { + position: fixed; + display: table; + width: 100%; + height: 100%; +} + +.displayContents > div { + display: table-row; + width: 100%; +} + +.headercontent { + display: table-cell; + width: 100%; + height: 2.5em; +} +.header nav { + display: flex; +} + +.header nav button { + border: none; + padding: 5px 0em 5px 4px; + display: flex; + text-align: right; + float: right; + outline: 1px solid transparent; + outline-offset: 2px !important; + color: var(--vscode-icon-foreground); + background: none; + margin: 0 0.3em; + border-radius: 5px; +} + +/* back and forward buttons are a bit off with vertical alignment */ +.header nav button.back-button, +.header nav button.forward-button { + padding: 4px 0em 6px 4px; +} + +.header nav button:hover:not(:disabled) { + cursor: pointer; + background: var(--vscode-toolbar-hoverBackground); +} + +.header nav button:disabled { + opacity: 0.5; +} + +.header nav button:focus { + outline-color: var(--vscode-focusBorder); +} + +.content iframe { + display: table-cell; + width: 100%; + height: 100%; + background: white; /* Browsers default to a white background */ +} + +.url-input { + flex: 1; +} + +input:not([type='checkbox']), +textarea { + display: block; + width: 100%; + border: none; + font-family: var(--vscode-font-family); + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + color: var(--vscode-input-foreground); + outline-color: var(--vscode-input-border); + background-color: var(--vscode-input-background); +} + +input::placeholder, +textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} + +#link-preview { + position: fixed; + bottom: -4px; + left: 0px; + padding: 3px 5px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-size: 9px; + box-shadow: 1px -1px 5px rgb(129, 129, 129); + border-radius: 1px; + opacity: 0; +} + +.find-container { + position: absolute; + margin-top: 3px; + right: 15px; + opacity: 0; +} +.find { + border-radius: 2px; + padding: 5px; + background: var(--vscode-editor-background); + margin-top: 0.5em; + right: 0px; + display: block; +} + +.find-input { + flex: 1; +} +.find nav { + display: flex; +} +.find button i { + display: flex; + margin-right: 0.3em; + padding: 0px; +} + +.find-result { + position: relative; + right: 22px; + top: 1px; +} + +.find-result i { + position: absolute; + display: flex; + padding: 4px 0em 4px 0px; + background-color: var(--vscode-input-background); +} + +.extras-menu { + position: absolute; + margin-top: 2.5em; + right: 0px; + opacity: 1; +} +.extras-menu table { + background-color: var(--vscode-input-background); + padding: 0.5em 0; + box-shadow: 0 0px 5px rgb(0 0 0 / 0.2); +} + +.extras-menu button { + background-color: var(--vscode-toolbar-background); + color: var(--vscode-toolbar-foreground); + padding: 7px 15px; + width: 100%; + margin: 0px; + text-align: left; + border: none; + outline: inherit; + font-weight: normal; +} + +.extras-menu button:focus { + background-color: var(--vscode-toolbar-hoverBackground); +} diff --git a/media/runtime.js b/media/runtime.js new file mode 100644 index 000000000..a7db06405 --- /dev/null +++ b/media/runtime.js @@ -0,0 +1,520 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-env browser */ +/* global acquireVsCodeApi, WS_URL */ + +// This script will be run within the webview itself +(function () { + const KEY_ENTER = 'Enter', + KEY_LEFT = 'ArrowLeft', + KEY_UP = 'ArrowUp', + KEY_RIGHT = 'ArrowRight', + KEY_DOWN = 'ArrowDown', + vscode = acquireVsCodeApi(), + connection = new WebSocket(WS_URL), + navGroups = { + 'leftmost-nav': true, + 'extra-menu-nav': false, + 'find-nav': true, + }; + let fadeLinkID = null, + onlyCtrlDown = false; + + onLoad(); + + /** + * @description run on load. + */ + function onLoad() { + for (const groupClassName in navGroups) { + const leftRight = navGroups[groupClassName]; + handleNavGroup(getNavGroupElems(groupClassName), leftRight); + } + + connection.addEventListener('error', (e) => { + console.log('WebSocket error: '); + console.log(e); + }); + connection.addEventListener('message', (e) => handleSocketMessage(e.data)); + + document.addEventListener('DOMContentLoaded', function (e) { + vscode.postMessage({ + command: 'refresh-back-forward-buttons', + }); + }); + + addNavButtonListeners(); + + document.getElementById('url-input').addEventListener('keydown', (e) => { + if (checkKeyCodeDetected(e, KEY_ENTER)) { + goToUrl(); + } + }); + + // set up key to dismiss find + document.getElementById('find-box').addEventListener('keydown', (e) => { + if ( + !document.getElementById('find-box').hidden && + e.key == 'Escape' && + !onlyCtrlDown + ) { + hideFind(); + } + }); + + // set up keys for navigating find + document.getElementById('find-input').addEventListener('keydown', (e) => { + if (checkKeyCodeDetected(e, KEY_ENTER)) { + findNext(); + } + }); + + // listen for CTRL+F for opening the find menu + document.addEventListener('keydown', (e) => { + onlyCtrlDown = (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey; + if ((e.key == 'F' || e.key == 'f') && onlyCtrlDown) { + showFind(); + } + }); + + document.addEventListener('keyup', (e) => { + onlyCtrlDown = (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey; + }); + + document.getElementById('more').addEventListener('keydown', (e) => { + if (!document.getElementById('extras-menu-pane').hidden) { + const menuNavGroup = getNavGroupElems('extra-menu-nav'); + if (checkKeyCodeDetected(e, KEY_DOWN)) { + menuNavGroup[0].focus(); + } else if (checkKeyCodeDetected(e, KEY_UP)) { + menuNavGroup[menuNavGroup.length - 1].focus(); + } + } + }); + + window.addEventListener('message', (event) => { + handleMessage(event.data); // The json data that the extension sent + }); + + document.getElementById('hostedContent').contentWindow.postMessage( + { + command: 'setup-parent-listener', + }, + '*' + ); + } + + /** + * + * @param {string} groupClassName the class name that is applied to elements of this nav group + * @returns + */ + function getNavGroupElems(groupClassName) { + return Array.from(document.getElementsByClassName(groupClassName)); + } + + /** + * @param {string} url the URL to use to set the URL bar. + */ + function setURLBar(url) { + document.getElementById('url-input').value = decodeURI(url); + } + + /** + * @description Update the webview's state with the current pathname to allow correct serialize/deserialize. + * @param {string} pathname + */ + function updateState(pathname) { + vscode.setState({ currentAddress: decodeURI(pathname) }); + } + + function goToUrl() { + const linkTarget = document.getElementById('url-input').value; + vscode.postMessage({ + command: 'go-to-file', + text: linkTarget, + }); + } + + /** + * @param {any} event the event processed + * @param {number} key the key to check for + * @returns whether the event includes the key pressed. + */ + function checkKeyCodeDetected(event, key) { + return event.key === key; + } + + /** + * Add keyboard listeners to navigation keys to allow arrow key navigation in the button groups. + * @param {HTMLElement[]} nav the navigation elements. + * @param {boolean} useRightLeft whether or not to navigate using right/left arrows. If false, uses up/down arrows. + */ + function handleNavGroup(nav, useRightLeft) { + for (const [currIndex, elem] of nav.entries()) { + elem.addEventListener('keydown', (e) => { + if (checkKeyCodeDetected(e, useRightLeft ? KEY_LEFT : KEY_UP)) { + moveFocusNav(false, nav, currIndex); + } else if ( + checkKeyCodeDetected(e, useRightLeft ? KEY_RIGHT : KEY_DOWN) + ) { + moveFocusNav(true, nav, currIndex); + } + }); + } + } + + /** + * Move the focus appropriately based on left/right action. + * @param {boolean} right whether to shift the focus right (!right will assume moving left). + * @param {HTMLElement[]} nav the navigation elements. + * @param {number} startIndex the index of the current HTMLElement focused (in `nav` array). + */ + function moveFocusNav(right, nav, startIndex) { + // logic behind shifting focus based on arrow-keys + let numDisabled = 0, + modifier = right ? 1 : -1, + index = startIndex, + newIndex = index; + do { + newIndex = Number(index) + modifier; + if (newIndex >= nav.length) { + newIndex = 0; + } else if (newIndex < 0) { + newIndex = nav.length - 1; + } + index = newIndex; + numDisabled++; + } while (nav[newIndex].disabled && numDisabled < nav.length); + + if (numDisabled < nav.length) { + nav[index].focus(); + } + } + + /** + * @description adjust the tab indices of the navigation buttons based on which buttons are disabled. + */ + function adjustTabIndex() { + let reachedElem = false; + const leftMostNavGroup = getNavGroupElems('leftmost-nav'); + for (const elem of leftMostNavGroup) { + if (!elem.disabled) { + if (reachedElem) { + elem.tabIndex = -1; + } else { + elem.tabIndex = 0; + reachedElem = true; + } + } + } + } + + /** + * @description handle messages coming from WebSocket. Usually are messages notifying of non-injectable files + * that the extension should be aware of. + * @param {any} data + */ + function handleSocketMessage(data) { + const parsedMessage = JSON.parse(data); + switch (parsedMessage.command) { + case 'foundNonInjectable': + // if the file we went to is not injectable, make sure to add it to history manually + vscode.postMessage({ + command: 'add-history', + text: JSON.stringify({ + path: parsedMessage.path, + port: parsedMessage.port, + }), + }); + return; + } + } + + /** + * @description handle messages coming from the child frame and extension. + * @param {any} message + */ + function handleMessage(message) { + switch (message.command) { + // from extension + case 'refresh': + document.getElementById('hostedContent').contentWindow.postMessage( + JSON.stringify({ + command: 'setup-parent-listener', + }), + '*' + ); + break; + // from extension + case 'changed-history': { + const msgJSON = JSON.parse(message.text); + if (msgJSON.element) { + document.getElementById(msgJSON.element).disabled = msgJSON.disabled; + } + adjustTabIndex(); + break; + } + // from extension + case 'set-url': { + const msgJSON = JSON.parse(message.text); + // setting a new address, ensure that previous link preview is gone + document.getElementById('link-preview').hidden = true; + setURLBar(msgJSON.fullPath); + updateState(msgJSON.pathname); + break; + } + // from child iframe + case 'did-keydown': { + handleKeyEvent('keydown', message.key); + break; + } + // from child iframe + case 'did-keyup': { + handleKeyEvent('keyup', message.key); + break; + } + // from child iframe + case 'update-path': { + const msgJSON = JSON.parse(message.text); + vscode.postMessage({ + command: 'update-path', + text: message.text, + }); + setURLBar(msgJSON.path.href); + updateState(msgJSON.path.pathname); + + // remove link preview box from last page. + document.getElementById('link-preview').hidden = true; + break; + } + // from child iframe + case 'link-hover-start': { + if (message.text.trim().length) { + document.getElementById('link-preview').innerText = message.text; + fadeElement(true, document.getElementById('link-preview')); + } + break; + } + // from child iframe + case 'link-hover-end': { + if (!document.getElementById('link-preview').hidden) { + fadeElement(false, document.getElementById('link-preview')); + } + break; + } + // from child iframe + case 'open-external-link': { + vscode.postMessage({ + command: 'open-browser' + }); + break; + } + // from child iframe + case 'console': { + vscode.postMessage({ + command: 'console', + text: message.text, + }); + break; + } + // from child iframe + case 'perform-url-check': { + const sendData = { + command: 'urlCheck', + url: message.text, + }; + connection.send(JSON.stringify(sendData)); + break; + } + // from child iframe + case 'show-find-icon': { + const codicon = document.getElementById('find-result-icon'); + const iconClass = message.text ? 'codicon-pass' : 'codicon-error'; + + if (!codicon.classList.contains(iconClass)) { + codicon.className = `codicon ${iconClass}`; + document.getElementById('find-result').hidden = true; + fadeElement(true, document.getElementById('find-result')); + } + break; + } + // from child iframe + case 'show-find': { + showFind(); + break; + } + } + } + + /** + * @description show the find menu + */ + function showFind() { + if (document.getElementById('find-box').hidden) { + fadeElement(true, document.getElementById('find-box')); + } + document.getElementById('find-input').focus(); + } + + /** + * @description hide the find menu + */ + function hideFind() { + if (!document.getElementById('find-box').hidden) { + fadeElement(false, document.getElementById('find-box')); + document.getElementById('find-result').hidden = true; + } + } + + /** + * @description Fade in or out the link preview. + * @param {boolean} appear whether or not it should be fade from `hide -> show`; otherwise, will fade from `show -> hide`. + */ + function fadeElement(appear, elem) { + const initOpacity = appear ? 0 : 1; + const finalOpacity = appear ? 1 : 0; + + elem.style.opacity = initOpacity; + clearInterval(fadeLinkID); + if (appear) { + elem.hidden = false; + } + + fadeLinkID = setInterval(function () { + if (elem.style.opacity == finalOpacity) { + clearInterval(fadeLinkID); + if (!appear) { + elem.hidden = true; + } + } else { + elem.style.opacity = + parseFloat(elem.style.opacity) + parseFloat(appear ? 0.1 : -0.1); + } + }, 25); + } + + /** + * @description highlight the next find result on the page. + */ + function findNext() { + document.getElementById('hostedContent').contentWindow.postMessage( + { + command: 'find-next', + text: document.getElementById('find-input').value, + }, + '*' + ); + } + + /** + * @description highlight the previous find result on the page. + */ + function findPrev() { + document.getElementById('hostedContent').contentWindow.postMessage( + { + command: 'find-prev', + text: document.getElementById('find-input').value, + }, + '*' + ); + } + + /** + * @description Add click/keyboard listeners to all toolbar buttons. + */ + function addNavButtonListeners() { + document.getElementById('back').addEventListener('click', () => { + vscode.postMessage({ + command: 'go-back', + }); + }); + + document.getElementById('forward').addEventListener('click', () => { + vscode.postMessage({ + command: 'go-forward', + }); + }); + + document.getElementById('reload').addEventListener('click', () => { + document + .getElementById('hostedContent') + .contentWindow.postMessage({ command: 'refresh-forced' }, '*'); + document.getElementById('reload').blur(); + }); + + document.getElementById('browser-open').addEventListener('click', () => { + document.getElementById('extras-menu-pane').hidden = true; + vscode.postMessage({ + command: 'open-browser' + }); + }); + + // close extra-menu-pane whenever not clicking on it + // todo: fix to use addEventListener while still being able to hide menu on clicking elsewhere + document.body.onblur = function (e) { + document.getElementById('extras-menu-pane').hidden = true; + }; + + document.body.addEventListener('click', () => { + document.getElementById('extras-menu-pane').hidden = true; + }); + + document + .getElementById('extras-menu-pane') + .addEventListener('click', (e) => { + e.stopPropagation(); + }); + const menuNavGroup = getNavGroupElems('extra-menu-nav'); + + for (const menuNavItem of menuNavGroup) { + menuNavItem.addEventListener('mouseover', () => menuNavItem.focus()); + } + + document.getElementById('more').addEventListener('click', (e) => { + const menuPane = document.getElementById('extras-menu-pane'); + menuPane.hidden = !menuPane.hidden; + e.stopPropagation(); + }); + + document.getElementById('devtools-open').addEventListener('click', () => { + document.getElementById('extras-menu-pane').hidden = true; + vscode.postMessage({ + command: 'devtools-open', + text: '', + }); + }); + + document.getElementById('find').addEventListener('click', () => { + document.getElementById('extras-menu-pane').hidden = true; + showFind(); + }); + + document + .getElementById('find-next') + .addEventListener('click', () => findNext()); + + document + .getElementById('find-prev') + .addEventListener('click', () => findPrev()); + + document + .getElementById('find-x') + .addEventListener('click', () => hideFind()); + } + + /** + * @description Create/displatch a keyboard event coming from child iframe. + */ + function handleKeyEvent(type, event) { + const emulatedKeyboardEvent = new KeyboardEvent(type, event); + Object.defineProperty(emulatedKeyboardEvent, 'target', { + get: () => document, + }); + window.dispatchEvent(emulatedKeyboardEvent); + } +})(); diff --git a/src/client/extension.ts b/src/client/extension.ts index bc3460891..a97f6c2e1 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -45,6 +45,7 @@ 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"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -191,7 +192,7 @@ export async function activate( const orgID = orgDetails.OrgId; const artemisResponse = await ArtemisService.getArtemisResponse(orgID, _telemetry, ""); if (artemisResponse !== null && artemisResponse.response !== null) { - const { geoName, geoLongName } = artemisResponse.response; + const { geoName, geoLongName, clusterName, clusterNumber } = artemisResponse.response; const pacActiveAuth = await pacTerminal.getWrapper()?.activeAuth(); let AadIdObject, EnvID, TenantID; if ((pacActiveAuth && pacActiveAuth.Status === SUCCESS)) { @@ -207,7 +208,8 @@ export async function activate( EnvID: EnvID[0].Value, UserID: AadIdObject[0].Value, TenantID: TenantID[0].Value, - Region: artemisResponse.stamp + Region: artemisResponse.stamp, + Location: getECSOrgLocationValue(clusterName, clusterNumber) }, PowerPagesClientName, true); } diff --git a/src/common/ecs-features/ecsFeatureFlagFilters.ts b/src/common/ecs-features/ecsFeatureFlagFilters.ts index e21381a92..a32ed7ea2 100644 --- a/src/common/ecs-features/ecsFeatureFlagFilters.ts +++ b/src/common/ecs-features/ecsFeatureFlagFilters.ts @@ -25,5 +25,11 @@ export interface ECSAPIFeatureFlagFilters { */ Region: string; + /** + * Deployment cluster location + * @example NDE, WCDE, NCH, WCH, CAE, NAE, SBR, SCUS, ECA, CCA, SIN, CIN, CFR, SFR + */ + Location: string; + // TBD - more API call filters to be added later } diff --git a/src/common/ecs-features/ecsFeatureUtil.ts b/src/common/ecs-features/ecsFeatureUtil.ts index d8fb297c7..df0d7d653 100644 --- a/src/common/ecs-features/ecsFeatureUtil.ts +++ b/src/common/ecs-features/ecsFeatureUtil.ts @@ -14,7 +14,8 @@ export function createECSRequestURL(filters: ECSAPIFeatureFlagFilters, clientNam EnvironmentID: filters.EnvID, UserID: filters.UserID, TenantID: filters.TenantID, - region: filters.Region + region: filters.Region, + location: filters.Location, }; const queryString = Object.keys(queryParams) diff --git a/src/common/utilities/Utils.ts b/src/common/utilities/Utils.ts index 86887253f..853f97433 100644 --- a/src/common/utilities/Utils.ts +++ b/src/common/utilities/Utils.ts @@ -264,7 +264,7 @@ async function getFileContentByType(activeFileUri: vscode.Uri, componentType: st } //fetchRelatedFiles function based on component type -export async function fetchRelatedFiles(activeFileUri: vscode.Uri, componentType: string, fieldType: string, telemetry: ITelemetry, sessionId:string): Promise { +export async function fetchRelatedFiles(activeFileUri: vscode.Uri, componentType: string, fieldType: string, telemetry: ITelemetry, sessionId: string): Promise { try { const relatedFileTypes = relatedFilesSchema[componentType]?.[fieldType]; if (!relatedFileTypes) { @@ -286,7 +286,7 @@ export async function fetchRelatedFiles(activeFileUri: vscode.Uri, componentType } catch (error) { const message = (error as Error)?.message; telemetry.sendTelemetryErrorEvent(VSCODE_EXTENSION_COPILOT_CONTEXT_RELATED_FILES_FETCH_FAILED, { error: message, sessionId: sessionId }); - oneDSLoggerWrapper.getLogger().traceError(VSCODE_EXTENSION_COPILOT_CONTEXT_RELATED_FILES_FETCH_FAILED, message, error as Error, { sessionId:sessionId }, {}); + oneDSLoggerWrapper.getLogger().traceError(VSCODE_EXTENSION_COPILOT_CONTEXT_RELATED_FILES_FETCH_FAILED, message, error as Error, { sessionId: sessionId }, {}); return []; } } @@ -302,3 +302,21 @@ export function getFileNameFromUri(uri: vscode.Uri): string { export function getFolderPathFromUri(uri: vscode.Uri): string { return path.dirname(uri.fsPath); } + +export function getECSOrgLocationValue(clusterName: string, clusterNumber: string): string { + // Find the position of the identifier in the input string + const identifierPosition = clusterName.indexOf("il" + clusterNumber); + + // If the identifier is not found, return an empty string + if (identifierPosition === -1) { + return ''; + } + + // Calculate the starting position of the substring "SIN" or "SEAS" or "SFR" in the input string + const startPosition = identifierPosition + clusterNumber.length; + + // Extract the substring "sin" from the input string + const extractedSubstring = clusterName.substring(startPosition); + + return extractedSubstring; +} diff --git a/src/common/utilities/dispose.ts b/src/common/utilities/dispose.ts new file mode 100644 index 000000000..f5aef2fef --- /dev/null +++ b/src/common/utilities/dispose.ts @@ -0,0 +1,43 @@ +/* + * 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 function disposeAll(disposables: vscode.Disposable[]): void { + while (disposables.length) { + const item = disposables.pop(); + if (item) { + item.dispose(); + } + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: vscode.Disposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed(): boolean { + return this._isDisposed; + } +} diff --git a/src/web/client/WebExtensionContext.ts b/src/web/client/WebExtensionContext.ts index a67b95cad..3128fa2ed 100644 --- a/src/web/client/WebExtensionContext.ts +++ b/src/web/client/WebExtensionContext.ts @@ -109,6 +109,7 @@ class WebExtensionContext implements IWebExtensionContext { private _websiteLanguageCode: string; private _geoName: string; private _geoLongName: string; + private _clusterLocation: string; private _serviceEndpointCategory: ServiceEndpointCategory; private _telemetry: WebExtensionTelemetry; private _npsEligibility: boolean; @@ -206,6 +207,12 @@ class WebExtensionContext implements IWebExtensionContext { public set geoLongName(name: string) { this._geoLongName = name; } + public get clusterLocation() { + return this._clusterLocation; + } + public set clusterLocation(name: string) { + this._clusterLocation = name; + } public get serviceEndpointCategory() { return this._serviceEndpointCategory; } @@ -281,6 +288,7 @@ class WebExtensionContext implements IWebExtensionContext { this._websiteLanguageCode = ""; this._geoName = ""; this._geoLongName = ""; + this._clusterLocation = ""; this._serviceEndpointCategory = ServiceEndpointCategory.NONE; this._telemetry = new WebExtensionTelemetry(); this._npsEligibility = false; diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index b135a292f..cc99908ca 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -43,8 +43,9 @@ import { PowerPagesAppName, PowerPagesClientName } from "../../common/ecs-featur import { IPortalWebExtensionInitQueryParametersTelemetryData } from "../../common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryInterface"; import { ArtemisService } from "../../common/services/ArtemisService"; import { showErrorDialog } from "../../common/utilities/errorHandlerUtil"; -import { ServiceEndpointCategory } from "../../common/services/Constants"; import { EXTENSION_ID } from "../../common/constants"; +import { getECSOrgLocationValue } from "../../common/utilities/Utils"; +import { RuntimePreview } from "./webViews/RuntimePreview"; export function activate(context: vscode.ExtensionContext): void { // setup telemetry @@ -156,7 +157,8 @@ export function activate(context: vscode.ExtensionContext): void { EnvID: queryParamsMap.get(queryParameters.ENV_ID) as string, UserID: WebExtensionContext.userId, TenantID: queryParamsMap.get(queryParameters.TENANT_ID) as string, - Region: queryParamsMap.get(queryParameters.REGION) as string + Region: queryParamsMap.get(queryParameters.REGION) as string, + Location: queryParamsMap.get(queryParameters.GEO) as string }, PowerPagesClientName); @@ -260,9 +262,22 @@ export function registerCollaborationView() { } export function powerPagesNavigation() { + const openRuntimeInVscode = true; const powerPagesNavigationProvider = new PowerPagesNavigationProvider(); vscode.window.registerTreeDataProvider('powerpages.powerPagesFileExplorer', powerPagesNavigationProvider); - vscode.commands.registerCommand('powerpages.powerPagesFileExplorer.powerPagesRuntimePreview', () => powerPagesNavigationProvider.previewPowerPageSite()); + vscode.commands.registerCommand('powerpages.powerPagesFileExplorer.powerPagesRuntimePreview', () => openRuntimeInVscode ? new RuntimePreview( + vscode.window.createWebviewPanel( + 'runtimePreview', // Identifies the type of the webview. Used internally + 'Webview Example', // Title of the panel displayed to the user + vscode.ViewColumn.One, // Editor column to show the new webview panel in. + { + enableScripts: true, // Enable scripts in the webview + retainContextWhenHidden: true, + } + ), + WebExtensionContext.extensionUri, + "https://site-cwc5h.powerappsportals.com/"//WebExtensionContext.urlParametersMap.get(queryParameters.WEBSITE_PREVIEW_URL) as string + ) : powerPagesNavigationProvider.previewPowerPageSite()); vscode.commands.registerCommand('powerpages.powerPagesFileExplorer.backToStudio', () => powerPagesNavigationProvider.backToStudio()); WebExtensionContext.telemetry.sendInfoTelemetry(webExtensionTelemetryEventNames.WEB_EXTENSION_POWER_PAGES_WEB_VIEW_REGISTERED); } @@ -666,17 +681,19 @@ function isActiveDocument(fileFsPath: string): boolean { async function fetchArtemisData(orgId: string) { const artemisResponse = await ArtemisService.getArtemisResponse(orgId, WebExtensionContext.telemetry.getTelemetryReporter(), ""); - if (artemisResponse === null) { + if (artemisResponse === null || artemisResponse.response === null) { WebExtensionContext.telemetry.sendErrorTelemetry( webExtensionTelemetryEventNames.WEB_EXTENSION_ARTEMIS_RESPONSE_FAILED, fetchArtemisData.name, ARTEMIS_RESPONSE_FAILED ); + return; } - WebExtensionContext.geoName = artemisResponse?.response?.geoName ?? ""; - WebExtensionContext.geoLongName = artemisResponse?.response?.geoLongName ?? ""; - WebExtensionContext.serviceEndpointCategory = artemisResponse?.stamp ?? ServiceEndpointCategory.NONE; + WebExtensionContext.geoName = artemisResponse.response.geoName; + WebExtensionContext.geoLongName = artemisResponse.response.geoLongName; + WebExtensionContext.serviceEndpointCategory = artemisResponse.stamp; + WebExtensionContext.clusterLocation = getECSOrgLocationValue(artemisResponse.response.clusterName, artemisResponse.response.clusterNumber); } function logOneDSLogger(queryParamsMap: Map) { diff --git a/src/web/client/webViews/RuntimePreview.ts b/src/web/client/webViews/RuntimePreview.ts new file mode 100644 index 000000000..945fe6f2a --- /dev/null +++ b/src/web/client/webViews/RuntimePreview.ts @@ -0,0 +1,257 @@ +/* + * 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 { getNonce } from '../../../common/utilities/Utils'; +import { Disposable } from '../../../common/utilities/dispose'; + +export enum NavEditCommands { + DISABLE_BACK, + ENABLE_BACK, + DISABLE_FORWARD, + ENABLE_FORWARD, +} + +/** + * @description the object responsible for communicating messages to the webview. + */ +export class RuntimePreview extends Disposable { + public currentAddress: string; // encoded address + + constructor( + private readonly _panel: vscode.WebviewPanel, + private readonly _extensionUri: vscode.Uri, + private readonly _runtimeUri: string, + ) { + super(); + this.currentAddress = _runtimeUri; + + this._panel.webview.postMessage({ + command: 'set-url', + text: JSON.stringify({ + fullPath: this._runtimeUri, + pathname: this._runtimeUri, + }), + }); + + this._register( + this._panel.webview.onDidReceiveMessage((message) => + this._handleWebviewMessage(message) + ) + ); + + this._panel.webview.html = this._getHtmlForWebview(); + } + + /** + * @description generate the HTML to load in the webview; this will contain the full-page iframe with the hosted content, + * in addition to the top navigation bar. + */ + private _getHtmlForWebview(): string { + // Local path to main script run in the webview + const scriptPathOnDisk = vscode.Uri.joinPath( + this._extensionUri, + 'media', + 'runtime.js' + ); + + // And the uri we use to load this script in the webview + const scriptUri = this._panel.webview.asWebviewUri(scriptPathOnDisk); + + // Local path to css styles + const stylesPathMainPath = vscode.Uri.joinPath( + this._extensionUri, + 'media', + 'runtime.css' + ); + const codiconsPathMainPath = vscode.Uri.joinPath( + this._extensionUri, + 'src', 'common', 'copilot', 'assets', 'styles', 'codicon.css' + ); + + // Uri to load styles into webview + const stylesMainUri = this._panel.webview.asWebviewUri(stylesPathMainPath); + const codiconsUri = this._panel.webview.asWebviewUri(codiconsPathMainPath); + + // Use a nonce to only allow specific scripts to be run + const nonce = getNonce(); + + const back = vscode.l10n.t('Back'); + const forward = vscode.l10n.t('Forward'); + const reload = vscode.l10n.t('Reload'); + const more = vscode.l10n.t('More Browser Actions'); + const find_prev = vscode.l10n.t('Previous'); + const find_next = vscode.l10n.t('Next'); + const find_x = vscode.l10n.t('Close'); + const browser_open = vscode.l10n.t('Open in Browser'); + const find = vscode.l10n.t('Find in Page'); + const devtools_open = vscode.l10n.t('Open Devtools Pane'); + + return ` + + + + + + + + + + + + ${"Test [review"} + + +
+
+
+ + +
+ +
+
+ +
+
+ + + + + `; + } + + /** + * @description handle messages from the webview (see messages sent from `media/main.js`). + * @param {any} message the message from webview + */ + private async _handleWebviewMessage(message: any): Promise { + switch (message.command) { + case 'alert': + vscode.window.showErrorMessage(message.text); + return; + case 'update-path': { + // const msgJSON = JSON.parse(message.text); + // this._webviewComm.handleNewPageLoad( + // msgJSON.path.pathname, + // this.currentConnection, + // msgJSON.title + // ); + return; + } + case 'go-back': + //await this._webviewComm.goBack(); + return; + case 'go-forward': + //await this._webviewComm.goForwards(); + return; + case 'open-browser': + //await this._openCurrentAddressInExternalBrowser(); + return; + case 'add-history': { + // const msgJSON = JSON.parse(message.text); + // const connection = this._connectionManager.getConnectionFromPort( + // msgJSON.port + // ); + // await this._webviewComm.setUrlBar(msgJSON.path, connection); + return; + } + case 'refresh-back-forward-buttons': + // this._webviewComm.updateForwardBackArrows(); + return; + case 'go-to-file': + // await this._goToFullAddress(message.text); + return; + + case 'console': { + // const msgJSON = JSON.parse(message.text); + // this._handleConsole(msgJSON.type, msgJSON.data); + return; + } + case 'devtools-open': + vscode.commands.executeCommand( + 'workbench.action.webview.openDeveloperTools' + ); + return; + } + } +}