Integrate create new graph with extension and react webview, and also use OS credentials to store connection string. (#24)
* Integrate create new graph with extension and react webview, and also use OS credentials to store connection string. * move vscode away from global context. * fix audit issues.
This commit is contained in:
Родитель
bd0f208916
Коммит
5041915930
|
@ -9,4 +9,11 @@ export class GraphTopologyData {
|
|||
});
|
||||
return response?.value;
|
||||
}
|
||||
|
||||
public static putGraphTopology(iotHubData: IotHubData, deviceId: string, moduleId: string, graphData: any): Promise<MediaGraphTopology[]> {
|
||||
return iotHubData.directMethodCall(deviceId, moduleId, "GraphTopologySet", {
|
||||
"@apiVersion": Constants.ApiVersion.version1,
|
||||
...graphData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import * as vscode from "vscode";
|
||||
import { GraphTopologyData } from "../Data/GraphTolologyData";
|
||||
import { IotHubData } from "../Data/IotHubData";
|
||||
import { MediaGraphInstance, MediaGraphTopology } from "../lva-sdk/lvaSDKtypes";
|
||||
import { Constants } from "../Util/Constants";
|
||||
import Localizer from "../Util/Localizer";
|
||||
import { GraphEditorPanel } from "../Webview/GraphPanel";
|
||||
import { InstanceItem } from "./InstanceItem";
|
||||
import { INode } from "./Node";
|
||||
|
||||
export class GraphTopologyItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public iotHubData: IotHubData,
|
||||
public readonly deviceId: string,
|
||||
public readonly moduleId: string,
|
||||
public readonly graphTopology?: MediaGraphTopology,
|
||||
private readonly _graphInstances?: MediaGraphInstance[]
|
||||
) {
|
||||
super(graphTopology?.name ?? Localizer.localize("createGraphButton"), vscode.TreeItemCollapsibleState.Expanded);
|
||||
if (graphTopology) {
|
||||
this.iconPath = new vscode.ThemeIcon("primitive-square");
|
||||
} else {
|
||||
this.iconPath = new vscode.ThemeIcon("add");
|
||||
this.command = { title: Localizer.localize("createGraphButton"), command: "lvaTopologyEditor.start", arguments: [this] };
|
||||
this.collapsibleState = vscode.TreeItemCollapsibleState.None;
|
||||
}
|
||||
}
|
||||
|
||||
public getChildren(): Promise<INode[]> | INode[] {
|
||||
if (this.graphTopology == null || this._graphInstances == null) {
|
||||
return [];
|
||||
}
|
||||
return (
|
||||
this._graphInstances
|
||||
?.filter((instance) => {
|
||||
return instance?.properties?.topologyName === this.graphTopology?.name;
|
||||
})
|
||||
?.map((instance) => {
|
||||
return new InstanceItem(instance);
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public createNewGraphCommand(context: vscode.ExtensionContext) {
|
||||
const createGraphPanel = GraphEditorPanel.createOrShow(context.extensionPath);
|
||||
if (createGraphPanel) {
|
||||
createGraphPanel.registerPostMessage({
|
||||
name: Constants.PostMessageNames.closeWindow,
|
||||
callback: () => {
|
||||
createGraphPanel.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
createGraphPanel.registerPostMessage({
|
||||
name: Constants.PostMessageNames.saveGraph,
|
||||
callback: async (topology: any) => {
|
||||
GraphTopologyData.putGraphTopology(this.iotHubData, this.deviceId, this.moduleId, topology).then(
|
||||
(response) => {
|
||||
vscode.commands.executeCommand("moduleExplorer.refresh");
|
||||
createGraphPanel.dispose();
|
||||
},
|
||||
(error) => {
|
||||
// show errors
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import * as vscode from "vscode";
|
||||
import { GraphInstanceData } from "../Data/GraphInstanceData";
|
||||
import { IotHubData } from "../Data/IotHubData";
|
||||
import { Constants } from "../Util/Constants";
|
||||
import { CredentialStore } from "../Util/credentialStore";
|
||||
import { ExtensionUtils, LvaHubConfig } from "../Util/ExtensionUtils";
|
||||
import { HubItem } from "./HubItem";
|
||||
import { ModuleItem } from "./ModuleItem";
|
||||
|
@ -19,12 +19,11 @@ export default class ModuleExplorer implements vscode.TreeDataProvider<INode> {
|
|||
if (connectionConfig && connectionConfig.connectionString) {
|
||||
this._connectionConfig = connectionConfig;
|
||||
this._iotHubData = new IotHubData(connectionConfig.connectionString);
|
||||
// TODO add a command to clear connections
|
||||
} else {
|
||||
const connectionInfo = await ExtensionUtils.setConnectionString();
|
||||
this._iotHubData = connectionInfo.iotHubData;
|
||||
this._connectionConfig = connectionInfo.lvaHubConfig;
|
||||
this.context.globalState.update(Constants.LvaGlobalStateKey, this._connectionConfig);
|
||||
CredentialStore.setConnectionInfo(this.context, this._connectionConfig);
|
||||
}
|
||||
if (this._iotHubData && this._connectionConfig) {
|
||||
this._iotHubData = new IotHubData(this._connectionConfig.connectionString);
|
||||
|
|
|
@ -4,8 +4,8 @@ import { GraphTopologyData } from "../Data/GraphTolologyData";
|
|||
import { IotHubData } from "../Data/IotHubData";
|
||||
import { MediaGraphInstance } from "../lva-sdk/lvaSDKtypes";
|
||||
import { LvaHubConfig } from "../Util/ExtensionUtils";
|
||||
import { GraphTopologyItem } from "./GraphTopologyItem";
|
||||
import { INode } from "./Node";
|
||||
import { TopologyItem } from "./TopologyItem";
|
||||
|
||||
export class ModuleItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
|
@ -22,12 +22,10 @@ export class ModuleItem extends vscode.TreeItem {
|
|||
public getChildren(lvaHubConfig?: LvaHubConfig, graphInstances?: MediaGraphInstance[]): Promise<INode[]> | INode[] {
|
||||
return new Promise((resolve, reject) => {
|
||||
GraphTopologyData.getGraphTopologies(this.iotHubData, this.deviceId, this.moduleId).then((graphTopologies) => {
|
||||
const createGraphItem = new vscode.TreeItem("Create graph");
|
||||
createGraphItem.iconPath = new vscode.ThemeIcon("add");
|
||||
resolve([
|
||||
createGraphItem as any, // Testing in line command
|
||||
new GraphTopologyItem(this.iotHubData, this.deviceId, this.moduleId),
|
||||
...graphTopologies?.map((topology) => {
|
||||
return new TopologyItem(this.iotHubData, this.deviceId, this.moduleId, topology, graphInstances ?? []);
|
||||
return new GraphTopologyItem(this.iotHubData, this.deviceId, this.moduleId, topology, graphInstances ?? []);
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import * as vscode from "vscode";
|
||||
import { IotHubData } from "../Data/IotHubData";
|
||||
import { MediaGraphInstance, MediaGraphTopology } from "../lva-sdk/lvaSDKtypes";
|
||||
import { InstanceItem } from "./InstanceItem";
|
||||
import { INode } from "./Node";
|
||||
|
||||
export class TopologyItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public iotHubData: IotHubData,
|
||||
public readonly deviceId: string,
|
||||
public readonly moduleId: string,
|
||||
public readonly graphTopology: MediaGraphTopology,
|
||||
private readonly _graphInstances: MediaGraphInstance[]
|
||||
) {
|
||||
super(graphTopology.name, vscode.TreeItemCollapsibleState.Expanded);
|
||||
this.iconPath = new vscode.ThemeIcon("primitive-square");
|
||||
}
|
||||
|
||||
public getChildren(): Promise<INode[]> | INode[] {
|
||||
return (
|
||||
this._graphInstances
|
||||
?.filter((instance) => {
|
||||
return instance?.properties?.topologyName === this.graphTopology.name;
|
||||
})
|
||||
?.map((instance) => {
|
||||
return new InstanceItem(instance);
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,4 +15,11 @@ export class Constants {
|
|||
};
|
||||
|
||||
public static LvaGlobalStateKey = "lvaGlobalStateConfigKey";
|
||||
public static ExtensionId = "lva-edge-vscode-extension";
|
||||
|
||||
// have a copy of this in react code.
|
||||
public static PostMessageNames = {
|
||||
closeWindow: "closeWindow",
|
||||
saveGraph: "saveGraph"
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
"use strict";
|
||||
import * as keytar from "keytar";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import * as vscode from "vscode";
|
||||
import { Constants } from "./constants";
|
||||
import { DeviceConfig, LvaHubConfig } from "./ExtensionUtils";
|
||||
|
||||
interface CredentialLvaHubConfig {
|
||||
connectionStringKey: string;
|
||||
devices: DeviceConfig[];
|
||||
}
|
||||
|
||||
export class CredentialStore {
|
||||
public static async getConnectionInfo(context: vscode.ExtensionContext): Promise<LvaHubConfig> {
|
||||
const connectionInfo: CredentialLvaHubConfig | undefined = context.globalState.get(Constants.LvaGlobalStateKey);
|
||||
if (!connectionInfo) {
|
||||
return (null as unknown) as LvaHubConfig;
|
||||
}
|
||||
let connectionString: string | undefined | null = "";
|
||||
try {
|
||||
connectionString = await keytar.getPassword(Constants.ExtensionId, connectionInfo.connectionStringKey);
|
||||
} catch (error) {
|
||||
connectionString = context.globalState.get(connectionInfo.connectionStringKey);
|
||||
}
|
||||
|
||||
if (!connectionString) {
|
||||
return (null as unknown) as LvaHubConfig;
|
||||
}
|
||||
return { connectionString: connectionString as string, devices: connectionInfo.devices };
|
||||
}
|
||||
|
||||
public static async setConnectionInfo(context: vscode.ExtensionContext, connectionInfo: LvaHubConfig) {
|
||||
const connectionKey = uuid();
|
||||
|
||||
context.globalState.update(Constants.LvaGlobalStateKey, {
|
||||
connectionStringKey: connectionKey,
|
||||
devices: connectionInfo.devices
|
||||
});
|
||||
|
||||
try {
|
||||
await keytar.setPassword(Constants.ExtensionId, connectionKey, connectionInfo.connectionString);
|
||||
} catch (error) {
|
||||
context.globalState.update(connectionKey, connectionInfo.connectionString);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ export interface LvaHubConfig {
|
|||
devices: DeviceConfig[];
|
||||
}
|
||||
|
||||
interface DeviceConfig {
|
||||
export interface DeviceConfig {
|
||||
deviceId: string;
|
||||
modules: string[];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
import { filter } from "lodash";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
import Localizer from "../Util/Localizer";
|
||||
|
||||
interface PostMessage {
|
||||
name: string;
|
||||
callback?: (data?: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages graph editor webview panels
|
||||
*/
|
||||
export class GraphEditorPanel {
|
||||
/**
|
||||
* Track the currently panel. Only allow a single panel to exist at a time.
|
||||
*/
|
||||
public static currentPanel: GraphEditorPanel | undefined;
|
||||
|
||||
public static readonly viewType = "lvaTopologyEditor";
|
||||
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private readonly _extensionPath: string;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _registeredMessages: PostMessage[] = [];
|
||||
|
||||
public static createOrShow(extensionPath: string) {
|
||||
const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
|
||||
|
||||
// If we already have a panel, show it.
|
||||
if (GraphEditorPanel.currentPanel) {
|
||||
GraphEditorPanel.currentPanel._panel.reveal(column);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create a new panel.
|
||||
const panel = vscode.window.createWebviewPanel(GraphEditorPanel.viewType, Localizer.localize("lva-edge.webview.title"), column || vscode.ViewColumn.One, {
|
||||
// Enable javascript in the webview
|
||||
enableScripts: true,
|
||||
|
||||
// And restrict the webview to only loading content from our extension's `build` directory.
|
||||
localResourceRoots: [vscode.Uri.file(path.join(extensionPath, "build"))]
|
||||
});
|
||||
|
||||
GraphEditorPanel.currentPanel = new GraphEditorPanel(panel, extensionPath);
|
||||
return GraphEditorPanel.currentPanel;
|
||||
}
|
||||
|
||||
public static revive(panel: vscode.WebviewPanel, extensionPath: string) {
|
||||
GraphEditorPanel.currentPanel = new GraphEditorPanel(panel, extensionPath);
|
||||
}
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel, extensionPath: string) {
|
||||
this._panel = panel;
|
||||
this._extensionPath = extensionPath;
|
||||
|
||||
// Set the webview's initial html content
|
||||
this._update();
|
||||
|
||||
// Listen for when the panel is disposed
|
||||
// This happens when the user closes the panel or when the panel is closed programmatically
|
||||
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||
|
||||
// Update the content based on view changes
|
||||
this._panel.onDidChangeViewState(
|
||||
(e) => {
|
||||
if (this._panel.visible) {
|
||||
this._update();
|
||||
}
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
);
|
||||
|
||||
this._panel.webview.onDidReceiveMessage((message) => {
|
||||
const filteredEvents = this._registeredMessages.filter((event) => {
|
||||
return event.name === message.command;
|
||||
});
|
||||
if (filteredEvents?.length === 1 && filteredEvents[0].callback) {
|
||||
filteredEvents[0].callback(message.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerPostMessage(message: PostMessage) {
|
||||
this._registeredMessages.push(message);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
GraphEditorPanel.currentPanel = undefined;
|
||||
|
||||
// Clean up our resources
|
||||
this._panel.dispose();
|
||||
|
||||
while (this._disposables.length) {
|
||||
const x = this._disposables.pop();
|
||||
if (x) {
|
||||
x.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _update() {
|
||||
this._panel.title = Localizer.localize("lva-edge.webview.title");
|
||||
this._panel.webview.html = this._getHtmlForWebview();
|
||||
}
|
||||
|
||||
private _getResourceInjection(nonce: string, ending: string, template: (uri: vscode.Uri) => string) {
|
||||
const webview = this._panel.webview;
|
||||
// from the VS Code example, seems to have to be this way instead import
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const manifest = require(path.join(this._extensionPath, "build", "asset-manifest.json"));
|
||||
const fileNames = manifest.entrypoints.filter((fileName: string) => fileName.endsWith("." + ending));
|
||||
|
||||
return fileNames
|
||||
.map((fileName: string) => {
|
||||
// Local path to main script run in the webview
|
||||
const scriptPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, "build", fileName));
|
||||
|
||||
// And the uri we use to load this script in the webview
|
||||
const uri = webview.asWebviewUri(scriptPathOnDisk);
|
||||
|
||||
return template(uri);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
private _getHtmlForWebview() {
|
||||
const webview = this._panel.webview;
|
||||
|
||||
// Use a nonce to whitelist which scripts can be run
|
||||
const nonce = getNonce();
|
||||
|
||||
const stylesheetInjection = this._getResourceInjection(nonce, "css", (uri) => `<link nonce="${nonce}" href="${uri}" rel="stylesheet">`);
|
||||
|
||||
const scriptInjection = this._getResourceInjection(nonce, "js", (uri) => `<script nonce="${nonce}" src="${uri}"></script>`);
|
||||
|
||||
// The linter does not know of this since it is VS Code internal
|
||||
// so we set a fallback value
|
||||
const language = JSON.parse(process.env.VSCODE_NLS_CONFIG || "{}")["locale"];
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<!--
|
||||
Use a content security policy to only allow loading images from https or from our extension directory,
|
||||
and only allow scripts that have a specific nonce.
|
||||
-->
|
||||
<meta http-equiv="Content-Security-Policy" content="img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${Localizer.localize("lva-edge.webview.title")}</title>
|
||||
${stylesheetInjection}
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="${nonce}">
|
||||
__webpack_nonce__ = "${nonce}";
|
||||
__webpack_public_path__ = "${webview.asWebviewUri(vscode.Uri.file(path.join(this._extensionPath, "build")))}/";
|
||||
window.language = "${language}";
|
||||
</script>
|
||||
${scriptInjection}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
function getNonce() {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
168
ext/extension.ts
168
ext/extension.ts
|
@ -1,18 +1,19 @@
|
|||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
import { GraphTopologyItem } from "./ModuleExplorerPanel/GraphTopologyItem";
|
||||
import { HubItem } from "./ModuleExplorerPanel/HubItem";
|
||||
import ModuleExplorer from "./ModuleExplorerPanel/ModuleExplorer";
|
||||
import { Constants } from "./Util/Constants";
|
||||
import { LvaHubConfig } from "./Util/ExtensionUtils";
|
||||
import { CredentialStore } from "./Util/credentialStore";
|
||||
import Localizer from "./Util/Localizer";
|
||||
import { GraphEditorPanel } from "./Webview/GraphPanel";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const locale = JSON.parse(process.env.VSCODE_NLS_CONFIG || "{}")["locale"];
|
||||
Localizer.loadLocalization(locale, context.extensionPath);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("lvaTopologyEditor.start", () => {
|
||||
GraphEditorPanel.createOrShow(context.extensionPath);
|
||||
vscode.commands.registerCommand("lvaTopologyEditor.start", (newGraphItem: GraphTopologyItem) => {
|
||||
//GraphEditorPanel.createOrShow(context.extensionPath);
|
||||
newGraphItem.createNewGraphCommand(context);
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -28,7 +29,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
const moduleExplorer = new ModuleExplorer(context);
|
||||
vscode.window.registerTreeDataProvider("moduleExplorer", moduleExplorer);
|
||||
|
||||
const config = context.globalState.get<LvaHubConfig>(Constants.LvaGlobalStateKey);
|
||||
const config = await CredentialStore.getConnectionInfo(context);
|
||||
if (config) {
|
||||
moduleExplorer.setConnectionString(config);
|
||||
}
|
||||
|
@ -47,156 +48,3 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages graph editor webview panels
|
||||
*/
|
||||
class GraphEditorPanel {
|
||||
/**
|
||||
* Track the currently panel. Only allow a single panel to exist at a time.
|
||||
*/
|
||||
public static currentPanel: GraphEditorPanel | undefined;
|
||||
|
||||
public static readonly viewType = "lvaTopologyEditor";
|
||||
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private readonly _extensionPath: string;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public static createOrShow(extensionPath: string) {
|
||||
const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
|
||||
|
||||
// If we already have a panel, show it.
|
||||
if (GraphEditorPanel.currentPanel) {
|
||||
GraphEditorPanel.currentPanel._panel.reveal(column);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create a new panel.
|
||||
const panel = vscode.window.createWebviewPanel(GraphEditorPanel.viewType, Localizer.localize("lva-edge.webview.title"), column || vscode.ViewColumn.One, {
|
||||
// Enable javascript in the webview
|
||||
enableScripts: true,
|
||||
|
||||
// And restrict the webview to only loading content from our extension's `build` directory.
|
||||
localResourceRoots: [vscode.Uri.file(path.join(extensionPath, "build"))]
|
||||
});
|
||||
|
||||
GraphEditorPanel.currentPanel = new GraphEditorPanel(panel, extensionPath);
|
||||
}
|
||||
|
||||
public static revive(panel: vscode.WebviewPanel, extensionPath: string) {
|
||||
GraphEditorPanel.currentPanel = new GraphEditorPanel(panel, extensionPath);
|
||||
}
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel, extensionPath: string) {
|
||||
this._panel = panel;
|
||||
this._extensionPath = extensionPath;
|
||||
|
||||
// Set the webview's initial html content
|
||||
this._update();
|
||||
|
||||
// Listen for when the panel is disposed
|
||||
// This happens when the user closes the panel or when the panel is closed programmatically
|
||||
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
||||
|
||||
// Update the content based on view changes
|
||||
this._panel.onDidChangeViewState(
|
||||
(e) => {
|
||||
if (this._panel.visible) {
|
||||
this._update();
|
||||
}
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
GraphEditorPanel.currentPanel = undefined;
|
||||
|
||||
// Clean up our resources
|
||||
this._panel.dispose();
|
||||
|
||||
while (this._disposables.length) {
|
||||
const x = this._disposables.pop();
|
||||
if (x) {
|
||||
x.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _update() {
|
||||
this._panel.title = Localizer.localize("lva-edge.webview.title");
|
||||
this._panel.webview.html = this._getHtmlForWebview();
|
||||
}
|
||||
|
||||
private _getResourceInjection(nonce: string, ending: string, template: (uri: vscode.Uri) => string) {
|
||||
const webview = this._panel.webview;
|
||||
// from the VS Code example, seems to have to be this way instead import
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const manifest = require(path.join(this._extensionPath, "build", "asset-manifest.json"));
|
||||
const fileNames = manifest.entrypoints.filter((fileName: string) => fileName.endsWith("." + ending));
|
||||
|
||||
return fileNames
|
||||
.map((fileName: string) => {
|
||||
// Local path to main script run in the webview
|
||||
const scriptPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, "build", fileName));
|
||||
|
||||
// And the uri we use to load this script in the webview
|
||||
const uri = webview.asWebviewUri(scriptPathOnDisk);
|
||||
|
||||
return template(uri);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
private _getHtmlForWebview() {
|
||||
const webview = this._panel.webview;
|
||||
|
||||
// Use a nonce to whitelist which scripts can be run
|
||||
const nonce = getNonce();
|
||||
|
||||
const stylesheetInjection = this._getResourceInjection(nonce, "css", (uri) => `<link nonce="${nonce}" href="${uri}" rel="stylesheet">`);
|
||||
|
||||
const scriptInjection = this._getResourceInjection(nonce, "js", (uri) => `<script nonce="${nonce}" src="${uri}"></script>`);
|
||||
|
||||
// The linter does not know of this since it is VS Code internal
|
||||
// so we set a fallback value
|
||||
const language = JSON.parse(process.env.VSCODE_NLS_CONFIG || "{}")["locale"];
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<!--
|
||||
Use a content security policy to only allow loading images from https or from our extension directory,
|
||||
and only allow scripts that have a specific nonce.
|
||||
-->
|
||||
<meta http-equiv="Content-Security-Policy" content="img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${Localizer.localize("lva-edge.webview.title")}</title>
|
||||
${stylesheetInjection}
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="${nonce}">
|
||||
__webpack_nonce__ = "${nonce}";
|
||||
__webpack_public_path__ = "${webview.asWebviewUri(vscode.Uri.file(path.join(this._extensionPath, "build")))}/";
|
||||
window.language = "${language}";
|
||||
</script>
|
||||
${scriptInjection}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
function getNonce() {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
12
package.json
12
package.json
|
@ -10,10 +10,8 @@
|
|||
"Extension Packs"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onCommand:lvaTopologyEditor.start",
|
||||
"onWebviewPanel:lvaTopologyEditor",
|
||||
"onView:moduleExplorer",
|
||||
"onCommand:moduleExplorer.refresh"
|
||||
"onView:moduleExplorer"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -28,7 +26,7 @@
|
|||
"category": "%lva-edge.command.category%"
|
||||
},
|
||||
{
|
||||
"command": "moduleExplorer.refreshEntry",
|
||||
"command": "moduleExplorer.refresh",
|
||||
"title": "Refresh",
|
||||
"icon": {
|
||||
"dark": "ext/resources/dark/refresh.svg",
|
||||
|
@ -51,7 +49,7 @@
|
|||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
"command": "moduleExplorer.refreshEntry",
|
||||
"command": "moduleExplorer.refresh",
|
||||
"when": "view == moduleExplorer",
|
||||
"group": "navigation"
|
||||
}
|
||||
|
@ -83,16 +81,18 @@
|
|||
"refreshVSToken": "vsts-npm-auth -config .npmrc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/keytar": "^4.4.2",
|
||||
"@vienna/react-dag-editor": "^1.73.0",
|
||||
"azure-iothub": "^1.12.4",
|
||||
"dagre": "^0.8.5",
|
||||
"eslint": "^6.8.0",
|
||||
"keytar": "^6.0.1",
|
||||
"lodash": "^4.17.19",
|
||||
"office-ui-fabric-react": "^7.117.1",
|
||||
"react": "^16.13.1",
|
||||
"react-accessible-tree": "^1.0.3",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-scripts": "^3.4.1"
|
||||
"react-scripts": "^3.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dagre": "^0.7.44",
|
||||
|
|
|
@ -6,5 +6,6 @@
|
|||
"connectionString.prompt": "Enter an IoT Hub connection string",
|
||||
"deviceList.prompt": "Select a device",
|
||||
"moduleList.prompt": "Select the live video analytics module",
|
||||
"iotHub.connectionString.validationMessageFormat": "The format should be :"
|
||||
"iotHub.connectionString.validationMessageFormat": "The format should be :",
|
||||
"createGraphButton": "Create graph"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ITheme } from "office-ui-fabric-react";
|
|||
import { ThemeProvider } from "office-ui-fabric-react/lib/Foundation";
|
||||
import React, { useEffect } from "react";
|
||||
import { IZoomPanSettings } from "@vienna/react-dag-editor";
|
||||
import { sampleTopology } from "./dev/sampleTopologies.js";
|
||||
import { GraphInstance } from "./editor/components/GraphInstance";
|
||||
import { GraphTopology } from "./editor/components/GraphTopology";
|
||||
import Graph from "./graph/Graph";
|
||||
|
@ -38,8 +37,6 @@ export const App: React.FunctionComponent<IProps> = (props) => {
|
|||
|
||||
if (props.graphData) {
|
||||
graph.setGraphData(props.graphData);
|
||||
} else {
|
||||
graph.setTopology(sampleTopology);
|
||||
}
|
||||
|
||||
// if there is no state to recover from (in props.graphData or zoomPanSettings), use default
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
RegisterPort,
|
||||
withDefaultPortsPosition
|
||||
} from "@vienna/react-dag-editor";
|
||||
import { ExtensionInteraction } from "../../extension/extensionInteraction";
|
||||
import Graph from "../../graph/Graph";
|
||||
import Localizer from "../../localization/Localizer";
|
||||
import { graphTheme as theme } from "../editorTheme";
|
||||
|
@ -83,6 +84,10 @@ export const GraphTopology: React.FunctionComponent<IGraphTopologyProps> = (prop
|
|||
graph.setDescription(graphDescription);
|
||||
graph.setGraphDataFromICanvasData(data);
|
||||
const topology = graph.getTopology();
|
||||
const vscode = ExtensionInteraction.getVSCode();
|
||||
if (vscode) {
|
||||
vscode.postMessage({ command: "saveGraph", text: topology });
|
||||
}
|
||||
console.log(topology);
|
||||
};
|
||||
|
||||
|
@ -152,7 +157,13 @@ export const GraphTopology: React.FunctionComponent<IGraphTopologyProps> = (prop
|
|||
name={graphTopologyName}
|
||||
exportGraph={exportGraph}
|
||||
closeEditor={() => {
|
||||
alert("TODO: Close editor");
|
||||
console.log("try this one");
|
||||
const vscode = ExtensionInteraction.getVSCode();
|
||||
if (vscode) {
|
||||
vscode.postMessage({
|
||||
command: "closeWindow"
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Stack.Item grow>
|
||||
|
|
|
@ -1,34 +1,42 @@
|
|||
import Localizer from "../localization/Localizer";
|
||||
import { InitializationParameters } from "../types/vscodeDelegationTypes";
|
||||
|
||||
// VS Code exposes this function: https://code.visualstudio.com/api/references/vscode-api#WebviewPanelSerializer
|
||||
declare const acquireVsCodeApi: any;
|
||||
export class ExtensionInteraction {
|
||||
private static _vsCode: vscode;
|
||||
|
||||
export async function initalizeEnvironment(language: string) {
|
||||
await Localizer.loadUserLanguage(language);
|
||||
|
||||
return new Promise((resolve: (params: InitializationParameters) => void, reject) => {
|
||||
// Check if this is running in VS Code (might be developing in React)
|
||||
if (typeof acquireVsCodeApi === "function") {
|
||||
(function () {
|
||||
const vscode = acquireVsCodeApi();
|
||||
const oldState = vscode.getState() || {};
|
||||
|
||||
// Handle messages sent from the extension to the webview
|
||||
window.addEventListener("message", (event) => {
|
||||
const message = event.data;
|
||||
// use message.command
|
||||
});
|
||||
|
||||
resolve({
|
||||
state: oldState,
|
||||
vsCodeSetState: vscode.setState
|
||||
});
|
||||
})();
|
||||
} else {
|
||||
// We won't save/restore state in browser, use noop function
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
resolve({ state: {}, vsCodeSetState: () => {} });
|
||||
public static getVSCode() {
|
||||
if (typeof this._vsCode == "undefined" && typeof acquireVsCodeApi == "function") {
|
||||
this._vsCode = acquireVsCodeApi();
|
||||
}
|
||||
});
|
||||
return this._vsCode;
|
||||
}
|
||||
|
||||
public static async initializeEnvironment(language: string) {
|
||||
await Localizer.loadUserLanguage(language);
|
||||
|
||||
return new Promise((resolve: (params: InitializationParameters) => void, reject) => {
|
||||
// Check if this is running in VS Code (might be developing in React)
|
||||
const vscode = this.getVSCode();
|
||||
if (vscode) {
|
||||
(function () {
|
||||
const oldState = vscode.getState() || {};
|
||||
|
||||
// Handle messages sent from the extension to the webview
|
||||
window.addEventListener("message", (event) => {
|
||||
const message = event.data;
|
||||
// use message.command
|
||||
});
|
||||
|
||||
resolve({
|
||||
state: oldState,
|
||||
vsCodeSetState: vscode.setState
|
||||
});
|
||||
})();
|
||||
} else {
|
||||
// We won't save/restore state in browser, use noop function
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
resolve({ state: {}, vsCodeSetState: () => {} });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ import "./scripts/formatString";
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
import { initalizeEnvironment } from "./extension/extensionInteraction";
|
||||
import { ExtensionInteraction } from "./extension/extensionInteraction";
|
||||
import { InitializationParameters } from "./types/vscodeDelegationTypes";
|
||||
|
||||
initalizeEnvironment((window as any).language).then((params: InitializationParameters) => {
|
||||
ExtensionInteraction.initializeEnvironment((window as any).language).then((params: InitializationParameters) => {
|
||||
// if we are running in VS Code and have stored state, we can recover it from state
|
||||
// vsCodeSetState will allow for setting that state
|
||||
// saving and restoring state happens when the webview loses and regains focus
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
interface vscode {
|
||||
postMessage(message: { command: string; text?: any }): void;
|
||||
getState();
|
||||
setState(state: any);
|
||||
}
|
||||
|
||||
declare const acquireVsCodeApi;
|
Загрузка…
Ссылка в новой задаче