Skip to content

Commit

Permalink
Fix interactive R and Python plots on Server Web and Workbench (#4855)
Browse files Browse the repository at this point in the history
### Description

- part of #4274
- addresses #4804
- should unblock #4806, where the new plotly rendering method relies on
a proxy server instead of an HTML file proxy

#### Changes

- add a new command to create a generic proxy server
`positronProxy.startHttpProxyServer`
- rename the command `positronProxy.stopHelpProxyServer` to
`positronProxy.stopProxyServer` since it is not help-specific
- rename `resources/scripts.html` to `resources/scripts_help.html` since
it is help-specific
- move the src/href rewriting to a private reusable function
`rewriteUrlsWithProxyPath`, which is now used by the generic http proxy
and the help proxy `contentRewriter`
- update `src/vs/code/browser/workbench/workbench.ts` to resolve the uri
while maintaining the uri's original path, query string and fragment
strings (NOTE: needs to be contributed upstream)
- update
`src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts`
to choose between starting an HTML file proxy if a file is being served
or a generic http proxy if the content is not a file

### QA Notes

On Server Web and Workbench, running the following in the corresponding
consoles:

##### Python

`pip install plotly nbformat pandas`

```python
import plotly.express as px
fig = px.bar(x=["a", "b", "c"], y=[1, 3, 2])
fig.show()
```

##### R

`install.packages('plotly')`

```r
library(plotly)
fig <- plot_ly(data = iris, x = ~Sepal.Length, y = ~Petal.Length)
fig
```

#### Expected Result

The corresponding interactive plots should display in the plots pane and
be interact-able!
  • Loading branch information
sharon-wang authored Oct 1, 2024
1 parent 4351851 commit 833a9b3
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 37 deletions.
1 change: 1 addition & 0 deletions extensions/positron-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"onCommand:positronProxy.startHelpProxyServer",
"onCommand:positronProxy.setHelpProxyServerStyles",
"onCommand:positronProxy.startHtmlProxyServer",
"onCommand:positronProxy.startHttpProxyServer",
"onStartupFinished"
],
"main": "./out/extension.js",
Expand Down
14 changes: 11 additions & 3 deletions extensions/positron-proxy/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,19 @@ export function activate(context: vscode.ExtensionContext) {
)
);

// Register the positronProxy.stopHelpProxyServer command and add its disposable.
// Register the positronProxy.startHttpProxyServer command and add its disposable.
context.subscriptions.push(
vscode.commands.registerCommand(
'positronProxy.stopHelpProxyServer',
(targetOrigin: string) => positronProxy.stopHelpProxyServer(targetOrigin)
'positronProxy.startHttpProxyServer',
async (targetOrigin: string) => await positronProxy.startHttpProxyServer(targetOrigin)
)
);

// Register the positronProxy.stopProxyServer command and add its disposable.
context.subscriptions.push(
vscode.commands.registerCommand(
'positronProxy.stopProxyServer',
(targetOrigin: string) => positronProxy.stopProxyServer(targetOrigin)
)
);

Expand Down
90 changes: 66 additions & 24 deletions extensions/positron-proxy/src/positronProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class PositronProxy implements Disposable {
//#region Private Properties

/**
* Gets or sets a value which indicates whether the resources/scripts.html file has been loaded.
* Gets or sets a value which indicates whether the resources/scripts_{TYPE}.html files have been loaded.
*/
private _scriptsFileLoaded = false;

Expand Down Expand Up @@ -141,11 +141,13 @@ export class PositronProxy implements Disposable {
* @param context The extension context.
*/
constructor(private readonly context: ExtensionContext) {
// Try to load the resources/scripts.html file and the elements within it. This will either
// Try to load the resources/scripts_{TYPE}.html files and the elements within them. This will either
// work or it will not work, but there's not sense in trying it again, if it doesn't.

// Load the scripts_help.html file for the help proxy server.
try {
// Load the resources/scripts.html scripts file.
const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts.html');
// Load the resources/scripts_help.html scripts file.
const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts_help.html');
const scripts = fs.readFileSync(scriptsPath).toString('utf8');

// Get the elements from the scripts file.
Expand All @@ -159,7 +161,7 @@ export class PositronProxy implements Disposable {
this._helpStyleOverrides !== undefined &&
this._helpScript !== undefined;
} catch (error) {
console.log(`Failed to load the resources/scripts.html file.`);
console.log(`Failed to load the resources/scripts_help.html file.`);
}
}

Expand Down Expand Up @@ -225,36 +227,21 @@ export class PositronProxy implements Disposable {
</head>`
);

// When running on Web, we need to prepend root-relative URLs with the proxy path,
// because the help proxy server is running at a different origin than the target origin.
// When running on Desktop, we don't need to do this, because the help proxy server is
// running at the same origin as the target origin (localhost).
if (vscode.env.uiKind === vscode.UIKind.Web) {
// Prepend root-relative URLs with the proxy path. The proxy path may look like
// /proxy/<PORT> or a different proxy path if an external uri is used.
response = response.replace(
// This is icky and we should use a proper HTML parser, but it works for now.
// Possible sources of error are: whitespace differences, single vs. double
// quotes, etc., which are not covered in this regex.
// Regex translation: look for src="/ or href="/ and replace it with
// src="<PROXY_PATH> or href="<PROXY_PATH> respectively.
/(src|href)="\/([^"]+)"/g,
`$1="${proxyPath}/$2"`
);
}
// Rewrite the URLs with the proxy path.
response = this.rewriteUrlsWithProxyPath(response, proxyPath);

// Return the response.
return response;
});
}

/**
* Stops a help proxy server.
* Stops a proxy server.
* @param targetOrigin The target origin.
* @returns A value which indicates whether the proxy server for the target origin was found and
* stopped.
*/
stopHelpProxyServer(targetOrigin: string): boolean {
stopProxyServer(targetOrigin: string): boolean {
// See if we have a proxy server for the target origin. If we do, stop it.
const proxyServer = this._proxyServers.get(targetOrigin);
if (proxyServer) {
Expand Down Expand Up @@ -291,6 +278,32 @@ export class PositronProxy implements Disposable {
this._helpStyles = styles;
}

/**
* Starts an HTTP proxy server.
* @param targetOrigin The target origin.
* @returns The server origin.
*/
startHttpProxyServer(targetOrigin: string): Promise<string> {
// Start the proxy server.
return this.startProxyServer(
targetOrigin,
async (serverOrigin, proxyPath, url, contentType, responseBuffer) => {
// If this isn't 'text/html' content, just return the response buffer.
if (!contentType.includes('text/html')) {
return responseBuffer;
}

// Get the response.
let response = responseBuffer.toString('utf8');

// Rewrite the URLs with the proxy path.
response = this.rewriteUrlsWithProxyPath(response, proxyPath);

// Return the response.
return response;
});
}

//#endregion Public Methods

//#region Private Methods
Expand Down Expand Up @@ -369,5 +382,34 @@ export class PositronProxy implements Disposable {
});
}

/**
* Rewrites the URLs in the content.
* @param content The content.
* @param proxyPath The proxy path.
* @returns The content with the URLs rewritten.
*/
rewriteUrlsWithProxyPath(content: string, proxyPath: string): string {
// When running on Web, we need to prepend root-relative URLs with the proxy path,
// because the help proxy server is running at a different origin than the target origin.
// When running on Desktop, we don't need to do this, because the help proxy server is
// running at the same origin as the target origin (localhost).
if (vscode.env.uiKind === vscode.UIKind.Web) {
// Prepend root-relative URLs with the proxy path. The proxy path may look like
// /proxy/<PORT> or a different proxy path if an external uri is used.
return content.replace(
// This is icky and we should use a proper HTML parser, but it works for now.
// Possible sources of error are: whitespace differences, single vs. double
// quotes, etc., which are not covered in this regex.
// Regex translation: look for src="/ or href="/ and replace it with
// src="<PROXY_PATH> or href="<PROXY_PATH> respectively.
/(src|href)="\/([^"]+)"/g,
`$1="${proxyPath}/$2"`
);
}

// Return the content as-is.
return content;
}

//#endregion Private Methods
}
10 changes: 9 additions & 1 deletion src/vs/code/browser/workbench/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { create } from 'vs/workbench/workbench.web.main';
// --- Start PWB: proxy port url ---
import { extractLocalHostUriMetaDataForPortMapping, TunnelOptions, TunnelCreationOptions } from 'vs/platform/tunnel/common/tunnel';
import { transformPort } from './urlPorts';
// eslint-disable-next-line no-duplicate-imports
import { join } from 'vs/base/common/path';
// --- End PWB ---

interface ISecretStorageCrypto {
Expand Down Expand Up @@ -614,7 +616,13 @@ function readCookie(name: string): string | undefined {
.replace('/p/', '/proxy/')
.replace('{{port}}', localhostMatch.port.toString());
}
resolvedUri = URI.parse(new URL(renderedTemplate, mainWindow.location.href).toString());
// Update the authority and path of the URI to point to the proxy server. This
// retains the original query and fragment, while updating the authority and
// path to the proxy server.
resolvedUri = resolvedUri.with({
authority: mainWindow.location.host,
path: join(mainWindow.location.pathname, renderedTemplate, resolvedUri.path),
});
} else {
throw new Error(`Failed to resolve external URI: ${uri.toString()}. Could not determine base url because productConfiguration missing.`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ class PositronHelpService extends Disposable implements IPositronHelpService {
cleanupTargetOrigins.forEach(targetOrigin => {
if (!activeTargetOrigins.includes(targetOrigin)) {
this._commandService.executeCommand<boolean>(
'positronProxy.stopHelpProxyServer',
'positronProxy.stopProxyServer',
targetOrigin
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,28 @@ export class UiClientInstance extends Disposable {
}

/**
* Starts an HTML proxy server for the given HTML file.
* Starts a proxy server for the given HTML file or server url.
*
* @param htmlPath The path to the HTML file to open
* @returns A URI representing the HTML file
* @param targetPath The path to the HTML file or server url to open
* @returns A URI representing the HTML file or server url
*/
private async startHtmlProxyServer(htmlPath: string): Promise<URI> {
const url = await this._commandService.executeCommand<string>(
'positronProxy.startHtmlProxyServer',
htmlPath
);
private async startHtmlProxyServer(targetPath: string): Promise<URI> {
const uriScheme = URI.parse(targetPath).scheme;
let url;

if (uriScheme === 'file') {
// If the path is for a file, start an HTML proxy server.
url = await this._commandService.executeCommand<string>(
'positronProxy.startHtmlProxyServer',
targetPath
);
} else if (uriScheme === 'http' || uriScheme === 'https') {
// If the path is for a server, start a generic proxy server.
url = await this._commandService.executeCommand<string>(
'positronProxy.startHttpProxyServer',
targetPath
);
}

if (!url) {
throw new Error('Failed to start HTML file proxy server');
Expand Down

0 comments on commit 833a9b3

Please sign in to comment.