This commit is contained in:
Johannes Bader 2018-04-05 15:51:21 -07:00
Родитель 057a384935
Коммит 916ad587af
18 изменённых файлов: 1129 добавлений и 73 удалений

60
.gitignore поставляемый
Просмотреть файл

@ -1,59 +1 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
node_modules/

19
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,19 @@
// Place your settings in this file to overwrite default and user settings.
{
"editor.formatOnSave": true,
"[typescript]": {
"editor.formatOnPaste": true,
"editor.tabSize": 2,
"editor.detectIndentation": false
},
"files.exclude": {
"node_modules/**": true,
"src/**/*.js": true,
"src/**/*.js.map": true,
"src/**/*.d.ts": true,
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/.DS_Store": true
}
}

11
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "0.1.0",
"command": "tsc",
"isShellCommand": true,
"args": ["-w", "-p", "."],
"showOutput": "silent",
"isBackground": true,
"problemMatcher": "$tsc-watch"
}

Просмотреть файл

@ -1,14 +0,0 @@
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

12
index.js Normal file
Просмотреть файл

@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const electron = require("electron");
const child_process_1 = require("child_process");
if (typeof electron === "string") {
child_process_1.spawn(electron, [__filename], { stdio: ["ignore", "ignore", process.stderr, process.stdin, process.stdout] });
}
else {
electron.app.on("ready", () => {
require("./src/index");
});
}

16
index.ts Normal file
Просмотреть файл

@ -0,0 +1,16 @@
import * as electron from "electron";
import { spawn } from "child_process";
if (typeof electron === "string") {
// make sure that electron can be electron!
delete process.env['ELECTRON_RUN_AS_NODE'];
const proc = spawn(electron as any, [__filename], { stdio: ["ignore", "ignore", process.stderr, process.stdin, process.stdout] });
(<any>process).on('exit', (code) => {
proc.kill();
});
} else {
electron.app.on("ready", () => {
require("./src/index");
});
}

41
package.json Normal file
Просмотреть файл

@ -0,0 +1,41 @@
{
"name": "autorest-interactive",
"version": "0.1.0",
"description": "AutoRest Interactive View",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepare" : "tsc -p . ",
"start": "node index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/olydis/autorest-interactive.git"
},
"keywords": [
"autorest"
],
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/olydis/autorest-interactive/issues"
},
"homepage": "https://github.com/olydis/autorest-interactive#readme",
"devDependencies": {
"@types/d3": "^4.8.0",
"@types/electron": "^1.4.37",
"@types/jquery": "^2.0.43",
"@types/js-yaml": "^3.5.30",
"@types/jsonpath": "^0.1.29",
"@types/node": "^7.0.18",
"typescript" : "*"
},
"dependencies": {
"d3": "^4.8.0",
"electron": "^1.6.7",
"jquery": "^3.2.1",
"js-yaml": "^3.8.4",
"jsonpath": "^0.2.11",
"vscode-jsonrpc": "^3.2.0"
}
}

Двоичные данные
sample.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 282 KiB

Просмотреть файл

@ -0,0 +1,89 @@
<html>
<head>
<title>AutoRest Interactive</title>
<script>
var exports = {};
</script>
<script src="index.js"></script>
<style>
* {
box-sizing: border-box;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}
body {
padding: 0;
margin: 0;
}
#overlays>div {
position: fixed;
width: 100%;
bottom: 0;
left: 0;
top: 80px;
padding: 20px;
overflow-y: scroll;
overflow-x: auto;
background: white;
opacity: 0.95;
}
#overlays>div>h1 {
z-index: 10;
position: fixed;
padding: 20px;
margin: 0;
left: 0;
top: 0;
width: 100%;
background: white;
white-space: nowrap;
}
#overlays>div>h1>button {
width: 30px;
height: 30px;
margin-right: 20px;
vertical-align: bottom;
}
#overlays>div>div {}
#overlays>div>div>table {
white-space: nowrap;
}
svg {
font-size: 1px;
font-family: monospace;
}
#pipelineGraph {
width: 100%;
height: 100%;
}
.scalable:hover {
transform: scale(2);
cursor: pointer;
}
line.edge {
stroke: #CCC;
}
textarea {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<svg id="pipelineGraph"></svg>
<div id="overlays"></div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,284 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
const $ = require("jquery");
const d3 = require("d3");
const jsonpath_1 = require("jsonpath");
window.onerror = e => electron_1.remote.dialog.showErrorBox("Unhandled Error", e);
function remoteEval(expression) {
return electron_1.ipcRenderer.sendSync("remoteEval", expression);
}
function readFile(uri) {
return electron_1.ipcRenderer.sendSync("readFile", uri);
}
$(() => {
const startTime = remoteEval("startTime");
const pipeline = remoteEval("pipeline");
const depth = (node) => node.inputs.map(i => depth(pipeline[i]) + 1).reduce((a, b) => Math.max(a, b), 0);
const runningTime = node => {
const selfFinished = node.state.finishedAt;
if (!selfFinished) {
return null;
}
const previousFinished = node.inputs.map(x => pipeline[x].state.finishedAt).reduce((a, b) => !b ? undefined : Math.max(a, b), startTime) || selfFinished;
return ((selfFinished || Date.now()) - (previousFinished || selfFinished)) / 1000;
};
const nodes = Object.keys(pipeline).map(key => Object.assign(pipeline[key], { key: key, displayName: key.split("/") }));
const links = [].concat.apply([], nodes.map(node => node.inputs.map(input => {
return {
source: pipeline[input],
target: node
};
})));
// horiz layout
nodes.forEach(n => n.depth = depth(n));
nodes.forEach(n => n.x = 0);
nodes.forEach((n, i) => n.y = i / 10);
const width = nodes.map(x => x.depth).reduce((a, b) => Math.max(a, b), 0);
const height = width * 0.7;
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-1).distanceMin(10).distanceMin(30))
.force("link", d3.forceLink(links).distance(1).strength(1).iterations(10))
.force("y", d3.forceY(0))
.stop();
for (var i = 0; i < 1; ++i) {
simulation.tick();
nodes.forEach(n => n.y = Math.min(Math.max(n.y, -height / 2), height / 2));
for (let d = 0; d <= width; ++d) {
const nodeSet = nodes.filter(n => n.depth === d);
const meanY = nodeSet.map(n => n.y).reduce((a, b) => a + b, 0) / nodeSet.length;
for (let i = 0; i < nodeSet.length; ++i) {
const n = nodeSet[i];
n.x = d - width / 2;
n.y = meanY + (i - nodeSet.length / 2) * 1.3;
}
}
}
const vis = d3.select("#pipelineGraph").attr("viewBox", `0 0 ${width + 2} ${height + 2}`).append("g").attr("transform", `translate(${width / 2 + 1},${height / 2 + 1})`);
let lastFootprint = null;
const render = () => {
const footprint = JSON.stringify(nodes) + JSON.stringify(links);
if (lastFootprint === footprint)
return;
lastFootprint = footprint;
const lineData = vis.selectAll("line.edge").data(links);
{
const enter = lineData.enter().append("line").attr("class", "edge").attr("stroke", "#000").attr("stroke-width", 0.02);
lineData.merge(enter)
.attr("x1", d => d.source.x.toFixed(3))
.attr("y1", d => d.source.y.toFixed(3))
.attr("x2", d => d.target.x.toFixed(3))
.attr("y2", d => d.target.y.toFixed(3));
lineData.exit().remove();
}
const nodeData = vis.selectAll(".node").data(nodes);
{
const enter = nodeData.enter()
.append("g")
.attr("class", "node")
.attr("stroke", "#000")
.attr("fill", "#FFF")
.attr("stroke-width", 0.03)
.attr("transform", d => `translate(${d.x},${d.y})`)
.append("g")
.attr("class", "scalable")
.on("click", showNodeDetails);
nodeData.exit().remove();
const update = nodeData.select(".scalable").merge(enter);
update.selectAll("*").remove();
update.append("circle")
.attr("r", "0.45em")
.attr("fill", d => d.state.state === "running"
? "#FFF"
: (d.state.state === "complete"
? `hsl(${(100 - 100 * Math.min(Math.max((runningTime(d) || 0) / 10, 0), 1)) | 0}, 100%, 80%)`
: "#000"));
update.append("text")
.attr("text-anchor", "middle")
.attr("style", d => `font-size: ${1.6 / (d.displayName.reduce((a, b) => Math.max(a, b.length), 0) + 1)}em`)
.html(d => d.displayName.map((l, i) => `<tspan x="0" y="${(i - (d.displayName.length - 1) / 2) * 1.3 + 0.3}em">${l}</tspan>`).join(""))
.attr("fill", d => d.state.state === "failed" ? "#FFF" : "#000").attr("stroke-width", 0);
update.append("text")
.attr("text-anchor", "middle")
.attr("y", "3.2em")
.attr("style", `font-size: 0.2em; font-weight: bold`)
.text(d => {
const sec = (runningTime(d) || 0).toFixed(1);
return sec === "0.0" || sec === "0.1" ? "" : `${sec}s`;
})
.attr("fill", "#000").attr("stroke-width", 0);
}
};
const update = () => {
const states = remoteEval(`tasks`);
for (const node of nodes) {
node.state = node.state || { state: "running" };
node.state.state = states[node.key]._state;
node.state.finishedAt = states[node.key]._finishedAt;
if (node.state.state === "complete" && !node.state.outputUris) {
node.state.outputUris = remoteEval(`tasks[${JSON.stringify(node.key)}]._result().map(x => x.key)`);
}
}
};
const refresh = () => { update(); render(); };
refresh();
setInterval(refresh, 100);
// $("#pipelineGraph").text(JSON.stringify(pipeline, null, 2));
// setInterval(() => {
// $("body").text(JSON.stringify(3));
// }, 1000);
});
function showOverlay(title, content) {
const overlay = $("<div>");
overlay.append($("<h1>")
.append($("<button>").text("X").click(() => overlay.remove()))
.append($("<span>").text(title)));
overlay.append($("<div>").append(content));
$("#overlays").append(overlay);
}
function showNodeDetails(node) {
const table = $("<table>");
table.append($("<tr>")
.append($("<td>").text("Plugin"))
.append($("<td>").text(node.pluginName)));
table.append($("<tr>")
.append($("<td>").text("Configuration Scope"))
.append($("<td>").text(jsonpath_1.stringify(["$"].concat(node.configScope)))));
table.append($("<tr>")
.append($("<td>").text("Output"))
.append($("<td>").append(node.state.outputUris.map(uri => $("<a>")
.attr("href", "#")
.text(uri)
.click(() => showUriDetails(uri))
.append($("<br>"))))));
// ext. extension
const extensionName = remoteEval(`(external[${JSON.stringify(node.pluginName)}] || {}).extensionName`);
if (extensionName) {
table.append($("<tr>")
.append($("<td>").text("Extension"))
.append($("<td>").append(extensionName)));
const traffic = remoteEval(`external[${JSON.stringify(node.pluginName)}].inspectTraffic`);
for (const [timeStamp, isCore2Ext, payload] of traffic) {
const payloadShortened = payload.length > 200 ? payload.substr(0, 200) + "..." : payload;
table.append($("<tr>").css("background", isCore2Ext ? "#FEE" : "#EFE")
.append($("<td>").text(new Date(timeStamp).toLocaleTimeString() + (isCore2Ext ? " (core => ext)" : " (ext => core)")))
.append($("<td>").append($("<a>")
.attr("href", "#")
.text(payloadShortened)
.click(() => showOverlay("", $("<textarea>").val(payload))))));
}
}
showOverlay(node.key, table);
}
function showUriDetails(uri) {
const content = $("<pre>").css("font-family", "monospace").text(readFile(uri));
content.click(e => {
const s = window.getSelection();
showBlameTreeDetails(remoteEval(`blame(${JSON.stringify(uri)}, ${JSON.stringify({ index: s.anchorOffset })})`));
});
showOverlay(uri, content);
}
function showBlameTreeDetails(blameTree) {
const content = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const depth = (blameTree) => blameTree.blaming.map(n => depth(n) + 1).reduce((a, b) => Math.max(a, b), 0);
const getNodes = blameTree => [blameTree].concat(...blameTree.blaming.map(getNodes));
const nodes = getNodes(blameTree).map(node => Object.assign(node, { displayName: [node.node.source, node.node.line + ":" + node.node.column] }));
const links = [].concat.apply([], nodes.map(node => node.blaming.map(input => {
return {
source: input,
target: node
};
})));
// horiz layout
nodes.forEach(n => n.depth = depth(n));
nodes.forEach((n, i) => n.x = i / 10);
const height = nodes.map(x => x.depth).reduce((a, b) => Math.max(a, b), 0);
let width = 5;
for (let i = 0; i <= height; ++i) {
width = Math.max(width, nodes.filter(n => n.depth === i).length);
}
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-1).distanceMin(10).distanceMin(30))
.force("link", d3.forceLink(links).distance(1).strength(1).iterations(10))
.force("x", d3.forceX(0))
.stop();
for (var i = 0; i < 1; ++i) {
simulation.tick();
nodes.forEach(n => n.x = Math.min(Math.max(n.x, -width / 2), width / 2));
for (let d = 0; d <= height; ++d) {
const nodeSet = nodes.filter(n => n.depth === d);
for (let i = 0; i < nodeSet.length; ++i) {
const n = nodeSet[i];
n.y = 5 * (d - height / 2);
n.x = 0 + (i - nodeSet.length / 2) * 1;
}
}
}
const vis = d3.select(content).attr("viewBox", `0 0 ${width + 2} ${5 * (height + 1)}`).append("g").attr("transform", `translate(${width / 2 + 1},${5 * (height + 1) / 2})`);
let lastFootprint = null;
const footprint = JSON.stringify(nodes) + JSON.stringify(links);
if (lastFootprint === footprint)
return;
lastFootprint = footprint;
const lineData = vis.selectAll("line.edge").data(links);
{
const enter = lineData.enter().append("line").attr("class", "edge").attr("stroke", "#000").attr("stroke-width", 0.02);
lineData.merge(enter)
.attr("x1", d => d.source.x.toFixed(3))
.attr("y1", d => d.source.y.toFixed(3))
.attr("x2", d => d.target.x.toFixed(3))
.attr("y2", d => d.target.y.toFixed(3));
lineData.exit().remove();
}
const nodeData = vis.selectAll(".node").data(nodes);
{
const enter = nodeData.enter()
.append("g")
.attr("writing-mode", "tb-rl")
.attr("class", "node")
.attr("stroke", "#000")
.attr("fill", "#FFF")
.attr("stroke-width", 0.03)
.attr("transform", d => `translate(${d.x},${d.y})`)
.append("g")
.attr("class", "scalable")
.on("click", d => electron_1.remote.dialog.showMessageBox({ title: d.node.source, message: d.node.name }));
const update = nodeData.select(".scalable").merge(enter);
update.selectAll("*").remove();
update.append("rect")
.attr("width", "0.9em")
.attr("height", "4.9em")
.attr("x", "-0.45em")
.attr("y", "-2.45em")
.attr("fill", "#FFF");
update.append("text")
.attr("text-anchor", "middle")
.attr("fill", "#000")
.attr("style", d => `font-size: ${10 / (d.displayName.reduce((a, b) => Math.max(a, b.length), 0) + 1)}em`)
.html(d => d.displayName.map((l, i) => `<tspan y="0" x="${-(i - (d.displayName.length - 1) / 2) * 1.3 - 0.3}em">${l}</tspan>`).join(""))
.attr("stroke-width", 0);
update.append("text")
.attr("text-anchor", "middle")
.attr("y", "3.2em")
.attr("style", `font-size: 0.2em; font-weight: bold`)
.text(d => "")
.attr("fill", "#000").attr("stroke-width", 0);
}
showOverlay(`Blame`, $(content));
}
// let deltaX: number | null = null;
// let deltaY: number | null = null;
// function moveNodeStart(node: PipelineNode, e: MouseEvent): void {
// deltaX = e.pageX - node.x;
// deltaY = e.pageY - node.y;
// }
// function moveNode(node: PipelineNode, e: MouseEvent): void {
// if (deltaX !== null) {
// node.x = e.pageX - deltaX;
// node.y = e.pageY - deltaY;
// }
// }
// function moveNodeEnd(node: PipelineNode, e: MouseEvent): void {
// deltaX = null;
// deltaY = null;
// }

Просмотреть файл

@ -0,0 +1,347 @@
import { ipcRenderer, remote } from "electron";
import * as $ from "jquery";
import * as d3 from "d3";
import { nodes, stringify } from "jsonpath";
import { JsonPath } from "../jsonrpc/types";
window.onerror = e => remote.dialog.showErrorBox("Unhandled Error", e);
function remoteEval(expression: string): any {
return ipcRenderer.sendSync("remoteEval", expression);
}
function readFile(uri: string): any {
return ipcRenderer.sendSync("readFile", uri);
}
type PipelineNodeState = {
state: "running" | "failed" | "complete";
outputUris?: string[];
finishedAt?: number;
};
type PipelineNode = {
outputArtifact?: string;
pluginName: string;
configScope: JsonPath;
inputs: string[];
// artificial
key: string;
displayName: string[];
x: number;
y: number;
depth: number;
state: PipelineNodeState;
};
$(() => {
const startTime = remoteEval("startTime");
const pipeline: { [name: string]: PipelineNode } = remoteEval("pipeline");
const depth = (node: PipelineNode) => node.inputs.map(i => depth(pipeline[i]) + 1).reduce((a, b) => Math.max(a, b), 0);
const runningTime: (node: PipelineNode) => number | null = node => {
const selfFinished = node.state.finishedAt;
if (!selfFinished) {
return null;
}
const previousFinished = node.inputs.map(x => pipeline[x].state.finishedAt).reduce((a, b) => !b ? undefined : Math.max(a, b), startTime) || selfFinished;
return ((selfFinished || Date.now()) - (previousFinished || selfFinished)) / 1000;
};
const nodes = Object.keys(pipeline).map(key => Object.assign(pipeline[key], { key: key, displayName: key.split("/") }));
const links: { source: PipelineNode, target: PipelineNode }[] = [].concat.apply([],
nodes.map(node => node.inputs.map(input => {
return {
source: pipeline[input],
target: node
};
})));
// horiz layout
nodes.forEach(n => n.depth = depth(n));
nodes.forEach(n => n.x = 0);
nodes.forEach((n, i) => n.y = i / 10);
const width = nodes.map(x => x.depth).reduce((a, b) => Math.max(a, b), 0);
const height = width * 0.7;
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-1).distanceMin(10).distanceMin(30))
.force("link", d3.forceLink(links).distance(1).strength(1).iterations(10))
.force("y", d3.forceY(0))
.stop();
for (var i = 0; i < 1; ++i) {
simulation.tick();
nodes.forEach(n => n.y = Math.min(Math.max(n.y, -height / 2), height / 2));
for (let d = 0; d <= width; ++d) {
const nodeSet = nodes.filter(n => n.depth === d);
const meanY = nodeSet.map(n => n.y).reduce((a, b) => a + b, 0) / nodeSet.length;
for (let i = 0; i < nodeSet.length; ++i) {
const n = nodeSet[i];
n.x = d - width / 2;
n.y = meanY + (i - nodeSet.length / 2) * 1.3;
}
}
}
const vis = d3.select("#pipelineGraph").attr("viewBox", `0 0 ${width + 2} ${height + 2}`).append("g").attr("transform", `translate(${width / 2 + 1},${height / 2 + 1})`);
let lastFootprint: string = null;
const render = () => {
const footprint = JSON.stringify(nodes) + JSON.stringify(links);
if (lastFootprint === footprint) return;
lastFootprint = footprint;
const lineData = vis.selectAll("line.edge").data(links);
{
const enter = lineData.enter().append("line").attr("class", "edge").attr("stroke", "#000").attr("stroke-width", 0.02);
lineData.merge(enter)
.attr("x1", d => d.source.x.toFixed(3))
.attr("y1", d => d.source.y.toFixed(3))
.attr("x2", d => d.target.x.toFixed(3))
.attr("y2", d => d.target.y.toFixed(3));
lineData.exit().remove();
}
const nodeData = vis.selectAll(".node").data(nodes);
{
const enter = nodeData.enter()
.append("g")
.attr("class", "node")
.attr("stroke", "#000")
.attr("fill", "#FFF")
.attr("stroke-width", 0.03)
.attr("transform", d => `translate(${d.x},${d.y})`)
.append("g")
.attr("class", "scalable")
.on("click", showNodeDetails)
// .on("mousedown", d => moveNodeStart(d, d3.event))
// .on("mousemove", d => moveNode(d, d3.event))
// .on("mouseup", d => moveNodeEnd(d, d3.event))
;
nodeData.exit().remove();
const update = nodeData.select(".scalable").merge(enter);
update.selectAll("*").remove();
update.append("circle")
.attr("r", "0.45em")
.attr("fill", d => d.state.state === "running"
? "#FFF"
: (d.state.state === "complete"
? `hsl(${(100 - 100 * Math.min(Math.max((runningTime(d) || 0) / 10, 0), 1)) | 0}, 100%, 80%)`
: "#000"));
update.append("text")
.attr("text-anchor", "middle")
.attr("style", d => `font-size: ${1.6 / (d.displayName.reduce((a, b) => Math.max(a, b.length), 0) + 1)}em`)
.html(d => d.displayName.map((l, i) => `<tspan x="0" y="${(i - (d.displayName.length - 1) / 2) * 1.3 + 0.3}em">${l}</tspan>`).join(""))
.attr("fill", d => d.state.state === "failed" ? "#FFF" : "#000").attr("stroke-width", 0);
update.append("text")
.attr("text-anchor", "middle")
.attr("y", "3.2em")
.attr("style", `font-size: 0.2em; font-weight: bold`)
.text(d => {
const sec = (runningTime(d) || 0).toFixed(1);
return sec === "0.0" || sec === "0.1" ? "" : `${sec}s`;
})
.attr("fill", "#000").attr("stroke-width", 0);
}
};
const update = () => {
const states = remoteEval(`tasks`);
for (const node of nodes) {
node.state = node.state || { state: "running" };
node.state.state = states[node.key]._state;
node.state.finishedAt = states[node.key]._finishedAt;
if (node.state.state === "complete" && !node.state.outputUris) {
node.state.outputUris = remoteEval(`tasks[${JSON.stringify(node.key)}]._result().map(x => x.key)`);
}
}
};
const refresh = () => { update(); render(); };
refresh();
setInterval(refresh, 100);
// $("#pipelineGraph").text(JSON.stringify(pipeline, null, 2));
// setInterval(() => {
// $("body").text(JSON.stringify(3));
// }, 1000);
});
function showOverlay(title: string, content: JQuery): void {
const overlay = $("<div>");
overlay.append($("<h1>")
.append($("<button>").text("X").click(() => overlay.remove()))
.append($("<span>").text(title)));
overlay.append($("<div>").append(content));
$("#overlays").append(overlay);
}
function showNodeDetails(node: PipelineNode): void {
const table = $("<table>");
table.append($("<tr>")
.append($("<td>").text("Plugin"))
.append($("<td>").text(node.pluginName)));
table.append($("<tr>")
.append($("<td>").text("Configuration Scope"))
.append($("<td>").text(stringify(["$"].concat(node.configScope as any)))));
table.append($("<tr>")
.append($("<td>").text("Output"))
.append($("<td>").append(node.state.outputUris.map(uri => $("<a>")
.attr("href", "#")
.text(uri)
.click(() => showUriDetails(uri))
.append($("<br>"))))));
// ext. extension
const extensionName = remoteEval(`(external[${JSON.stringify(node.pluginName)}] || {}).extensionName`);
if (extensionName) {
table.append($("<tr>")
.append($("<td>").text("Extension"))
.append($("<td>").append(extensionName)));
const traffic: [number, boolean, string][] = remoteEval(`external[${JSON.stringify(node.pluginName)}].inspectTraffic`);
for (const [timeStamp, isCore2Ext, payload] of traffic) {
const payloadShortened = payload.length > 200 ? payload.substr(0, 200) + "..." : payload;
table.append($("<tr>").css("background", isCore2Ext ? "#FEE" : "#EFE")
.append($("<td>").text(new Date(timeStamp).toLocaleTimeString() + (isCore2Ext ? " (core => ext)" : " (ext => core)")))
.append($("<td>").append($("<a>")
.attr("href", "#")
.text(payloadShortened)
.click(() => showOverlay("", $("<textarea>").val(payload))))));
}
}
showOverlay(node.key, table);
}
function showUriDetails(uri: string): void {
const content = $("<pre>").css("font-family", "monospace").text(readFile(uri));
content.click(e => {
const s = window.getSelection();
showBlameTreeDetails(remoteEval(`blame(${JSON.stringify(uri)}, ${JSON.stringify({ index: s.anchorOffset })})`));
});
showOverlay(uri, content);
}
type BlameTree = { node: any, blaming: BlameTree[] };
function showBlameTreeDetails(blameTree: BlameTree): void {
const content = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const depth = (blameTree: BlameTree) => blameTree.blaming.map(n => depth(n) + 1).reduce((a, b) => Math.max(a, b), 0);
const getNodes: (blameTree: BlameTree) => BlameTree[] = blameTree => [blameTree].concat(...blameTree.blaming.map(getNodes));
const nodes: (BlameTree & {
displayName: string[];
depth: number;
x: number;
y: number;
})[] = getNodes(blameTree).map(node => Object.assign(node, { displayName: [node.node.source, node.node.line + ":" + node.node.column] } as any));
const links: { source: PipelineNode, target: PipelineNode }[] = [].concat.apply([],
nodes.map(node => node.blaming.map(input => {
return {
source: input,
target: node
};
})));
// horiz layout
nodes.forEach(n => n.depth = depth(n));
nodes.forEach((n, i) => n.x = i / 10);
const height = nodes.map(x => x.depth).reduce((a, b) => Math.max(a, b), 0);
let width = 5;
for (let i = 0; i <= height; ++i) {
width = Math.max(width, nodes.filter(n => n.depth === i).length);
}
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-1).distanceMin(10).distanceMin(30))
.force("link", d3.forceLink(links).distance(1).strength(1).iterations(10))
.force("x", d3.forceX(0))
.stop();
for (var i = 0; i < 1; ++i) {
simulation.tick();
nodes.forEach(n => n.x = Math.min(Math.max(n.x, -width / 2), width / 2));
for (let d = 0; d <= height; ++d) {
const nodeSet = nodes.filter(n => n.depth === d);
for (let i = 0; i < nodeSet.length; ++i) {
const n = nodeSet[i];
n.y = 5 * (d - height / 2);
n.x = 0 + (i - nodeSet.length / 2) * 1;
}
}
}
const vis = d3.select(content).attr("viewBox", `0 0 ${width + 2} ${5 * (height + 1)}`).append("g").attr("transform", `translate(${width / 2 + 1},${5 * (height + 1) / 2})`);
let lastFootprint: string = null;
const footprint = JSON.stringify(nodes) + JSON.stringify(links);
if (lastFootprint === footprint) return;
lastFootprint = footprint;
const lineData = vis.selectAll("line.edge").data(links);
{
const enter = lineData.enter().append("line").attr("class", "edge").attr("stroke", "#000").attr("stroke-width", 0.02);
lineData.merge(enter)
.attr("x1", d => d.source.x.toFixed(3))
.attr("y1", d => d.source.y.toFixed(3))
.attr("x2", d => d.target.x.toFixed(3))
.attr("y2", d => d.target.y.toFixed(3));
lineData.exit().remove();
}
const nodeData = vis.selectAll(".node").data(nodes);
{
const enter = nodeData.enter()
.append("g")
.attr("writing-mode", "tb-rl")
.attr("class", "node")
.attr("stroke", "#000")
.attr("fill", "#FFF")
.attr("stroke-width", 0.03)
.attr("transform", d => `translate(${d.x},${d.y})`)
.append("g")
.attr("class", "scalable")
.on("click", d => remote.dialog.showMessageBox({ title: d.node.source, message: d.node.name }))
;
const update = nodeData.select(".scalable").merge(enter);
update.selectAll("*").remove();
update.append("rect")
.attr("width", "0.9em")
.attr("height", "4.9em")
.attr("x", "-0.45em")
.attr("y", "-2.45em")
.attr("fill", "#FFF");
update.append("text")
.attr("text-anchor", "middle")
.attr("fill", "#000")
.attr("style", d => `font-size: ${10 / (d.displayName.reduce((a, b) => Math.max(a, b.length), 0) + 1)}em`)
.html(d => d.displayName.map((l, i) => `<tspan y="0" x="${-(i - (d.displayName.length - 1) / 2) * 1.3 - 0.3}em">${l}</tspan>`).join(""))
.attr("stroke-width", 0);
update.append("text")
.attr("text-anchor", "middle")
.attr("y", "3.2em")
.attr("style", `font-size: 0.2em; font-weight: bold`)
.text(d => "")
.attr("fill", "#000").attr("stroke-width", 0);
}
showOverlay(`Blame`, $(content));
}
// let deltaX: number | null = null;
// let deltaY: number | null = null;
// function moveNodeStart(node: PipelineNode, e: MouseEvent): void {
// deltaX = e.pageX - node.x;
// deltaY = e.pageY - node.y;
// }
// function moveNode(node: PipelineNode, e: MouseEvent): void {
// if (deltaX !== null) {
// node.x = e.pageX - deltaX;
// node.y = e.pageY - deltaY;
// }
// }
// function moveNodeEnd(node: PipelineNode, e: MouseEvent): void {
// deltaX = null;
// deltaY = null;
// }

40
src/index.js Normal file
Просмотреть файл

@ -0,0 +1,40 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
const plugin_host_1 = require("./jsonrpc/plugin-host");
const fs_1 = require("fs");
const js_yaml_1 = require("js-yaml");
// required to actually keep process running when window is closed
electron_1.app.on('window-all-closed', () => { });
const pluginHost = new plugin_host_1.AutoRestPluginHost();
pluginHost.Add("autorest-interactive", (initiator) => __awaiter(this, void 0, void 0, function* () {
const win = new electron_1.BrowserWindow({});
win.maximize();
win.setMenu(null);
if (yield initiator.GetValue("debug")) {
win.webContents.openDevTools();
}
const readFileListener = (event, uri) => __awaiter(this, void 0, void 0, function* () {
event.returnValue = yield initiator.ReadFile(uri);
});
const remoteEvalListener = (event, expression) => __awaiter(this, void 0, void 0, function* () {
event.returnValue = js_yaml_1.safeLoad(yield initiator.GetValue("__status." + new Buffer(expression).toString("base64")));
});
electron_1.ipcMain.on("readFile", readFileListener);
electron_1.ipcMain.on("remoteEval", remoteEvalListener);
win.loadURL(`${__dirname}/autorest-interactive/index.html`);
yield new Promise(res => win.once("closed", res));
electron_1.ipcMain.removeListener("remoteEval", remoteEvalListener);
electron_1.ipcMain.removeListener("readFile", readFileListener);
}));
const parent_stdin = fs_1.createReadStream(null, { fd: 3 });
const parent_stdout = fs_1.createWriteStream(null, { fd: 4 });
pluginHost.Run(parent_stdin, parent_stdout);

32
src/index.ts Normal file
Просмотреть файл

@ -0,0 +1,32 @@
import { app, BrowserWindow, dialog, ipcMain } from "electron";
import { AutoRestPluginHost } from "./jsonrpc/plugin-host";
import { createReadStream, createWriteStream } from "fs";
import { safeLoad } from "js-yaml";
// required to actually keep process running when window is closed
app.on('window-all-closed', () => { })
const pluginHost = new AutoRestPluginHost();
pluginHost.Add("autorest-interactive", async initiator => {
const win = new BrowserWindow({});
win.maximize();
win.setMenu(null);
if (await initiator.GetValue("debug")) {
win.webContents.openDevTools();
}
const readFileListener = async (event, uri) => {
event.returnValue = await initiator.ReadFile(uri);
};
const remoteEvalListener = async (event, expression) => {
event.returnValue = safeLoad(await initiator.GetValue("__status." + new Buffer(expression).toString("base64")));
};
ipcMain.on("readFile", readFileListener);
ipcMain.on("remoteEval", remoteEvalListener);
win.loadURL(`${__dirname}/autorest-interactive/index.html`);
await new Promise<void>(res => win.once("closed", res));
ipcMain.removeListener("remoteEval", remoteEvalListener);
ipcMain.removeListener("readFile", readFileListener);
});
const parent_stdin = createReadStream(null, { fd: 3 });
const parent_stdout = createWriteStream(null, { fd: 4 });
pluginHost.Run(parent_stdin, parent_stdout);

Просмотреть файл

@ -0,0 +1,87 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
var IAutoRestPluginTarget_Types;
(function (IAutoRestPluginTarget_Types) {
IAutoRestPluginTarget_Types.GetPluginNames = new vscode_jsonrpc_1.RequestType0("GetPluginNames");
IAutoRestPluginTarget_Types.Process = new vscode_jsonrpc_1.RequestType2("Process");
})(IAutoRestPluginTarget_Types || (IAutoRestPluginTarget_Types = {}));
var IAutoRestPluginInitiator_Types;
(function (IAutoRestPluginInitiator_Types) {
IAutoRestPluginInitiator_Types.ReadFile = new vscode_jsonrpc_1.RequestType2("ReadFile");
IAutoRestPluginInitiator_Types.GetValue = new vscode_jsonrpc_1.RequestType2("GetValue");
IAutoRestPluginInitiator_Types.ListInputs = new vscode_jsonrpc_1.RequestType1("ListInputs");
IAutoRestPluginInitiator_Types.WriteFile = new vscode_jsonrpc_1.NotificationType4("WriteFile");
IAutoRestPluginInitiator_Types.Message = new vscode_jsonrpc_1.NotificationType2("Message");
})(IAutoRestPluginInitiator_Types || (IAutoRestPluginInitiator_Types = {}));
class AutoRestPluginHost {
constructor() {
this.plugins = {};
}
Add(name, handler) {
this.plugins[name] = handler;
}
Run(input = process.stdin, output = process.stdout) {
return __awaiter(this, void 0, void 0, function* () {
// connection setup
const channel = vscode_jsonrpc_1.createMessageConnection(input, output, {
error(message) { console.error("error: ", message); },
info(message) { console.error("info: ", message); },
log(message) { console.error("log: ", message); },
warn(message) { console.error("warn: ", message); }
});
channel.onRequest(IAutoRestPluginTarget_Types.GetPluginNames, () => __awaiter(this, void 0, void 0, function* () { return Object.keys(this.plugins); }));
channel.onRequest(IAutoRestPluginTarget_Types.Process, (pluginName, sessionId) => __awaiter(this, void 0, void 0, function* () {
try {
const handler = this.plugins[pluginName];
if (!handler) {
throw new Error(`Plugin host could not find requested plugin '${pluginName}'.`);
}
yield handler({
ReadFile(filename) {
return __awaiter(this, void 0, void 0, function* () {
return yield channel.sendRequest(IAutoRestPluginInitiator_Types.ReadFile, sessionId, filename);
});
},
GetValue(key) {
return __awaiter(this, void 0, void 0, function* () {
return yield channel.sendRequest(IAutoRestPluginInitiator_Types.GetValue, sessionId, key);
});
},
ListInputs() {
return __awaiter(this, void 0, void 0, function* () {
return yield channel.sendRequest(IAutoRestPluginInitiator_Types.ListInputs, sessionId);
});
},
WriteFile(filename, content, sourceMap) {
channel.sendNotification(IAutoRestPluginInitiator_Types.WriteFile, sessionId, filename, content, sourceMap);
},
Message(message) {
channel.sendNotification(IAutoRestPluginInitiator_Types.Message, sessionId, message);
}
});
return true;
}
catch (e) {
channel.sendNotification(IAutoRestPluginInitiator_Types.Message, sessionId, {
Channel: "fatal",
Text: "" + e,
Details: e
});
return false;
}
}));
// activate
channel.listen();
});
}
}
exports.AutoRestPluginHost = AutoRestPluginHost;

Просмотреть файл

@ -0,0 +1,95 @@
import {
createMessageConnection, Logger,
RequestType0, RequestType1, RequestType2,
NotificationType2, NotificationType4
} from "vscode-jsonrpc";
import { Readable } from "stream";
import { Mapping, Message, RawSourceMap } from "./types";
module IAutoRestPluginTarget_Types {
export const GetPluginNames = new RequestType0<string[], Error, void>("GetPluginNames");
export const Process = new RequestType2<string, string, boolean, Error, void>("Process");
}
interface IAutoRestPluginTarget {
GetPluginNames(): Promise<string[]>;
Process(pluginName: string, sessionId: string): Promise<boolean>;
}
module IAutoRestPluginInitiator_Types {
export const ReadFile = new RequestType2<string, string, string, Error, void>("ReadFile");
export const GetValue = new RequestType2<string, string, any, Error, void>("GetValue");
export const ListInputs = new RequestType1<string, string[], Error, void>("ListInputs");
export const WriteFile = new NotificationType4<string, string, string, Mapping[] | RawSourceMap | undefined, void>("WriteFile");
export const Message = new NotificationType2<string, Message, void>("Message");
}
interface IAutoRestPluginInitiator {
ReadFile(filename: string): Promise<string>;
GetValue(key: string): Promise<any>;
ListInputs(): Promise<string[]>;
WriteFile(filename: string, content: string, sourceMap?: Mapping[] | RawSourceMap): void;
Message(message: Message): void;
}
type AutoRestPluginHandler = (initiator: IAutoRestPluginInitiator) => Promise<void>;
export class AutoRestPluginHost {
private readonly plugins: { [name: string]: AutoRestPluginHandler } = {};
public Add(name: string, handler: AutoRestPluginHandler): void {
this.plugins[name] = handler;
}
public async Run(input: NodeJS.ReadableStream = process.stdin, output: NodeJS.WritableStream = process.stdout): Promise<void> {
// connection setup
const channel = createMessageConnection(
input,
output,
{
error(message) { console.error("error: ", message); },
info(message) { console.error("info: ", message); },
log(message) { console.error("log: ", message); },
warn(message) { console.error("warn: ", message); }
}
);
channel.onRequest(IAutoRestPluginTarget_Types.GetPluginNames, async () => Object.keys(this.plugins));
channel.onRequest(IAutoRestPluginTarget_Types.Process, async (pluginName: string, sessionId: string) => {
try {
const handler = this.plugins[pluginName];
if (!handler) {
throw new Error(`Plugin host could not find requested plugin '${pluginName}'.`);
}
await handler({
async ReadFile(filename: string): Promise<string> {
return await channel.sendRequest(IAutoRestPluginInitiator_Types.ReadFile, sessionId, filename);
},
async GetValue(key: string): Promise<any> {
return await channel.sendRequest(IAutoRestPluginInitiator_Types.GetValue, sessionId, key);
},
async ListInputs(): Promise<string[]> {
return await channel.sendRequest(IAutoRestPluginInitiator_Types.ListInputs, sessionId);
},
WriteFile(filename: string, content: string, sourceMap?: Mapping[] | RawSourceMap): void {
channel.sendNotification(IAutoRestPluginInitiator_Types.WriteFile, sessionId, filename, content, sourceMap);
},
Message(message: Message): void {
channel.sendNotification(IAutoRestPluginInitiator_Types.Message, sessionId, message);
}
});
return true;
} catch (e) {
channel.sendNotification(IAutoRestPluginInitiator_Types.Message, sessionId, <Message>{
Channel: "fatal" as any,
Text: "" + e,
Details: e
});
return false;
}
});
// activate
channel.listen();
}
}

2
src/jsonrpc/types.js Normal file
Просмотреть файл

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

37
src/jsonrpc/types.ts Normal file
Просмотреть файл

@ -0,0 +1,37 @@
/* line: 1-based, column: 0-based */
export type Position = {
line: number; // 1-based
column: number; // 0-based
} | { path?: JsonPath };
export type JsonPath = (string | number)[];
export interface SourceLocation {
document: string;
Position: Position;
}
export interface Message {
Channel: "information" | "warning" | "error" | "debug" | "verbose";
Key?: Iterable<string>;
Details?: any;
Text: string;
Source?: Array<SourceLocation>;
}
export interface RawSourceMap {
file?: string;
sourceRoot?: string;
version: string;
sources: string[];
names: string[];
sourcesContent?: string[];
mappings: string;
}
export interface Mapping {
generated: Position;
original: Position;
source: string;
name?: string;
}

16
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": [
"es2016",
"dom"
],
"module": "commonjs",
"types": [
"node"
],
"target": "es2016"
},
"exclude": [
"node_modules"
]
}