Skip to content

Commit

Permalink
feat: Packed webview js and updated security for it
Browse files Browse the repository at this point in the history
Christopher-R-Perkins committed Jul 14, 2024
1 parent c46dfd2 commit 3ae30f1
Showing 6 changed files with 110 additions and 100 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ out/
build/
*.tsbuildinfo
.history/
dag-packed.js

# env
.env
@@ -38,4 +39,4 @@ build/
bundled/libs/
**/__pycache__
**/.pytest_cache
**/.vs
**/.vs
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -419,6 +419,7 @@
"axios": "^1.6.7",
"dagre": "^0.8.5",
"fs-extra": "^11.2.0",
"svg-pan-zoom": "github:bumbu/svg-pan-zoom",
"svgdom": "^0.1.19",
"vscode-languageclient": "^9.0.1"
}
2 changes: 2 additions & 0 deletions resources/dag-view/dag.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import svgPanZoom from 'svg-pan-zoom';

(() => {
const dag = document.querySelector('#dag');
const panZoom = svgPanZoom(dag);
161 changes: 79 additions & 82 deletions src/commands/pipelines/DagRender.ts
Original file line number Diff line number Diff line change
@@ -15,31 +15,37 @@ import * as vscode from 'vscode';
import * as Dagre from 'dagre';
import { ArrayXY, SVG, registerWindow } from '@svgdotjs/svg.js';
import { PipelineTreeItem, ServerDataProvider } from '../../views/activityBar';
import { DagResp, DagNode } from '../../types/PipelineTypes';
import { PipelineRunDag, DagNode } from '../../types/PipelineTypes';
import { LSClient } from '../../services/LSClient';
import { ServerStatus } from '../../types/ServerInfoTypes';
import { JsonObject } from '../../views/panel/panelView/PanelTreeItem';
import { PanelDataProvider } from '../../views/panel/panelView/PanelDataProvider';

interface Edge {
from: string;
points: ArrayXY[];
}
const ROOT_PATH = ['resources', 'dag-view'];
const CSS_FILE = 'dag.css';
const JS_FILE = 'dag-packed.js';
const ICONS_DIRECTORY = '/resources/dag-view/icons/';

export default class DagRenderer {
private static instance: DagRenderer | undefined;
private openPanels: { [id: string]: vscode.WebviewPanel };
private extensionPath: string;
private createSVGWindow: Function = () => {};
private iconSvgs: { [name: string]: string } = {};
private root: vscode.Uri;
private javaScript: vscode.Uri;
private css: vscode.Uri;

constructor(context: vscode.ExtensionContext) {
DagRenderer.instance = this;
this.openPanels = {};
this.extensionPath = context.extensionPath;
this.root = vscode.Uri.joinPath(context.extensionUri, ...ROOT_PATH);
this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE);
this.css = vscode.Uri.joinPath(this.root, CSS_FILE);

this.loadSvgWindowLib();
this.loadIcons();
this.loadIcons(context.extensionPath + ICONS_DIRECTORY);
}

/**
* Retrieves a singleton instance of DagRenderer
*
@@ -75,47 +81,46 @@ export default class DagRenderer {
vscode.ViewColumn.One,
{
enableScripts: true,
localResourceRoots: [this.root],
}
);

panel.webview.html = this.getLoadingContent();

const status = ServerDataProvider.getInstance().getCurrentStatus() as ServerStatus;
const dashboardUrl = status.dashboard_url;
const deploymentType = status.deployment_type;
const runUrl = deploymentType === 'other' ? '' : `${dashboardUrl}/runs/${node.id}?tab=overview`;

const client = LSClient.getInstance();
const dataPanel = PanelDataProvider.getInstance();

panel.webview.onDidReceiveMessage(async message => {
switch (message.command) {
case 'update':
this.renderDag(panel, node);
break;

case 'step':
const stepData = await LSClient.getInstance().sendLsClientRequest<JsonObject>(
'getPipelineRunStep',
[message.id]
);
PanelDataProvider.getInstance().setData(
{ runUrl, ...stepData },
'Pipeline Run Step Data'
);
const stepData = await client.sendLsClientRequest<JsonObject>('getPipelineRunStep', [
message.id,
]);

dataPanel.setData({ runUrl, ...stepData }, 'Pipeline Run Step Data');
vscode.commands.executeCommand('zenmlPanelView.focus');
break;

case 'artifact':
const artifactData = await LSClient.getInstance().sendLsClientRequest<JsonObject>(
const artifactData = await client.sendLsClientRequest<JsonObject>(
'getPipelineRunArtifact',
[message.id]
);

if (deploymentType === 'cloud') {
const artifactUrl = `${dashboardUrl}/artifact-versions/${message.id}?tab=overview`;
PanelDataProvider.getInstance().setData(
{ artifactUrl, ...artifactData },
'Artifact Version Data'
);
dataPanel.setData({ artifactUrl, ...artifactData }, 'Artifact Version Data');
} else {
PanelDataProvider.getInstance().setData(
{ runUrl, ...artifactData },
'Artifact Version Data'
);
dataPanel.setData({ runUrl, ...artifactData }, 'Artifact Version Data');
}

vscode.commands.executeCommand('zenmlPanelView.focus');
@@ -144,28 +149,20 @@ export default class DagRenderer {
}

private async renderDag(panel: vscode.WebviewPanel, node: PipelineTreeItem) {
panel.webview.html = this.getLoadingContent();

const client = LSClient.getInstance();

let dagData: DagResp;
let dagData: PipelineRunDag;
try {
dagData = await client.sendLsClientRequest<DagResp>('getPipelineRunDag', [node.id]);
dagData = await client.sendLsClientRequest<PipelineRunDag>('getPipelineRunDag', [node.id]);
} catch (e) {
vscode.window.showErrorMessage(`Unable to receive response from Zenml server: ${e}`);
return;
}

const cssUri = panel.webview.asWebviewUri(this.css);
const jsUri = panel.webview.asWebviewUri(this.javaScript);
const graph = this.layoutDag(dagData);

const svg = await this.drawDag(dagData.nodes, graph, panel);

const cssOnDiskPath = vscode.Uri.file(this.extensionPath + '/resources/dag-view/dag.css');
const cssUri = panel.webview.asWebviewUri(cssOnDiskPath).toString();

const jsOnDiskPath = vscode.Uri.file(this.extensionPath + '/resources/dag-view/dag.js');
const jsUri = panel.webview.asWebviewUri(jsOnDiskPath).toString();

const svg = await this.drawDag(graph);
const updateButton = dagData.status === 'running' || dagData.status === 'initializing';
const title = `${dagData.name} - v${dagData.version}`;

@@ -178,6 +175,27 @@ export default class DagRenderer {
this.createSVGWindow = createSVGWindow;
}

private loadIcons(path: string): void {
const ICON_MAP = {
failed: 'alert.svg',
completed: 'check.svg',
cached: 'cached.svg',
initializing: 'initializing.svg',
running: 'play.svg',
database: 'database.svg',
dataflow: 'dataflow.svg',
};
Object.entries(ICON_MAP).forEach(async ([name, fileName]) => {
try {
const file = await fs.readFile(path + fileName);
this.iconSvgs[name] = file.toString();
} catch (e) {
this.iconSvgs[name] = '';
console.error(`Unable to load icon ${name}: ${e}`);
}
});
}

private deregisterDagPanel(runId: string) {
delete this.openPanels[runId];
}
@@ -194,42 +212,23 @@ export default class DagRenderer {
}, null);
}

private layoutDag(dagData: DagResp): Dagre.graphlib.Graph {
private layoutDag(dagData: PipelineRunDag): Dagre.graphlib.Graph {
const { nodes, edges } = dagData;
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: 'TB', ranksep: 35, nodesep: 5 });
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
graph.setGraph({ rankdir: 'TB', ranksep: 35, nodesep: 5 });

edges.forEach(edge => g.setEdge(edge.source, edge.target));
edges.forEach(edge => graph.setEdge(edge.source, edge.target));
nodes.forEach(node =>
g.setNode(node.id, { width: 300, height: node.type === 'step' ? 50 : 44 })
graph.setNode(node.id, { width: 300, height: node.type === 'step' ? 50 : 44, ...node })
);

Dagre.layout(g);
return g;
}

private loadIcons(): void {
const ICON_MAP = {
failed: 'alert.svg',
completed: 'check.svg',
cached: 'cached.svg',
initializing: 'initializing.svg',
running: 'play.svg',
database: 'database.svg',
dataflow: 'dataflow.svg',
};
const basePath = `${this.extensionPath}/resources/dag-view/icons/`;
Object.entries(ICON_MAP).forEach(async ([name, fileName]) => {
try {
const file = await fs.readFile(basePath + fileName);
this.iconSvgs[name] = file.toString();
} catch {
this.iconSvgs[name] = '';
}
});
Dagre.layout(graph);
return graph;
}

private calculateEdges = (g: Dagre.graphlib.Graph): Array<Edge> => {
private calculateEdges = (
g: Dagre.graphlib.Graph
): Array<{ from: string; points: ArrayXY[] }> => {
const edges = g.edges();
return edges.map(edge => {
const currentLine = g.edge(edge).points.map<ArrayXY>(point => [point.x, point.y]);
@@ -249,12 +248,7 @@ export default class DagRenderer {
});
};

private async drawDag(
nodes: Array<DagNode>,
graph: Dagre.graphlib.Graph,
panel: vscode.WebviewPanel
): Promise<string> {
// const uris = this.getIconUris(panel);
private async drawDag(graph: Dagre.graphlib.Graph): Promise<string> {
const window = this.createSVGWindow();
const document = window.document;

@@ -273,10 +267,10 @@ export default class DagRenderer {
.attr('data-from', edge.from);
});

const nodesGroup = canvas.group().attr('id', 'nodes');
const nodeGroup = canvas.group().attr('id', 'nodes');

nodes.forEach(node => {
const { width, height, x, y } = graph.node(node.id);
graph.nodes().forEach(nodeId => {
const node = graph.node(nodeId) as DagNode & ReturnType<typeof graph.node>;
let iconSVG: string;
let status: string = '';
const executionId = { attr: '', value: node.data.execution_id };
@@ -294,15 +288,17 @@ export default class DagRenderer {
}
}

const container = nodesGroup
.foreignObject(width, height)
.translate(x - width / 2, y - height / 2);
const container = nodeGroup
.foreignObject(node.width, node.height)
.translate(node.x - node.width / 2, node.y - node.height / 2);

const div = container.element('div').attr('class', 'node').attr('data-id', node.id);

const box = div
.element('div')
.attr('class', node.type)
.attr(executionId.attr, executionId.value);

const icon = SVG(iconSVG);
box.add(SVG(icon).attr('class', `icon ${status}`));
box.element('p').words(node.data.name);
@@ -316,6 +312,7 @@ export default class DagRenderer {
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Secuirty-Policy" content="default-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loading</title>
<style>
@@ -348,8 +345,8 @@ export default class DagRenderer {
title,
}: {
svg: string;
cssUri: string;
jsUri: string;
cssUri: vscode.Uri;
jsUri: vscode.Uri;
updateButton: boolean;
title: string;
}): string {
@@ -358,8 +355,8 @@ export default class DagRenderer {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Secuirty-Policy" content="default-src 'none'; script-src ${jsUri}; style-src ${cssUri};">
<link rel="stylesheet" href="${cssUri}">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/svg-pan-zoom.min.js"></script>
<title>DAG</title>
</head>
<body>
10 changes: 4 additions & 6 deletions src/types/PipelineTypes.ts
Original file line number Diff line number Diff line change
@@ -34,11 +34,8 @@ export interface PipelineRun {
pythonVersion: string;
}

interface DagNodeBase {
export interface DagStep {
id: string;
}

export interface DagStep extends DagNodeBase {
type: 'step';
data: {
execution_id: string;
@@ -47,7 +44,8 @@ export interface DagStep extends DagNodeBase {
};
}

export interface DagArtifact extends DagNodeBase {
export interface DagArtifact {
id: string;
type: 'artifact';
data: {
execution_id: string;
@@ -64,7 +62,7 @@ export interface DagEdge {
target: string;
}

export interface DagResp {
export interface PipelineRunDag {
nodes: Array<DagNode>;
edges: Array<DagEdge>;
status: string;
33 changes: 22 additions & 11 deletions webpack.config.js
Original file line number Diff line number Diff line change
@@ -10,22 +10,22 @@ const path = require('path');
/** @type WebpackConfig */
const extensionConfig = {
target: 'node', // VS Code extensions run in a Node.js-context πŸ“– -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')

entry: './src/extension.ts', // the entry point of this extension, πŸ“– -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), πŸ“– -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
libraryTarget: 'commonjs2',
},
externals: {
vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, πŸ“– -> https://webpack.js.org/configuration/externals/
vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, πŸ“– -> https://webpack.js.org/configuration/externals/
// modules added here also need to be added in the .vscodeignore file
},
resolve: {
// support reading TypeScript and JavaScript files, πŸ“– -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js']
extensions: ['.ts', '.js'],
},
module: {
rules: [
@@ -34,15 +34,26 @@ const extensionConfig = {
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}
]
loader: 'ts-loader',
},
],
},
],
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
level: 'log', // enables logging required for problem matchers
},
};
module.exports = [ extensionConfig ];

const dagWebviewConfig = {
target: 'web',
mode: 'none',
entry: './resources/dag-view/dag.js',
output: {
path: path.resolve(__dirname, 'resources', 'dag-view'),
filename: 'dag-packed.js',
},
};

module.exports = [extensionConfig, dagWebviewConfig];

0 comments on commit 3ae30f1

Please sign in to comment.