From 4cd4df908709027cd71c6001a07d0bdf908abd49 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sat, 1 Oct 2022 19:14:23 +0200 Subject: [PATCH 01/57] initial devtools extension setupContextMenu manifest, devtools html/js, cells_panel html/js --- object_database/web/devtools/cell_panel.js | 1 + object_database/web/devtools/cells_panel.css | 4 ++++ object_database/web/devtools/cells_panel.html | 12 ++++++++++++ object_database/web/devtools/devtools.html | 6 ++++++ object_database/web/devtools/devtools_init.js | 10 ++++++++++ object_database/web/devtools/index.html | 11 +++++++++++ object_database/web/devtools/manifest.json | 6 ++++++ 7 files changed, 50 insertions(+) create mode 100644 object_database/web/devtools/cell_panel.js create mode 100644 object_database/web/devtools/cells_panel.css create mode 100644 object_database/web/devtools/cells_panel.html create mode 100644 object_database/web/devtools/devtools.html create mode 100644 object_database/web/devtools/devtools_init.js create mode 100644 object_database/web/devtools/index.html create mode 100644 object_database/web/devtools/manifest.json diff --git a/object_database/web/devtools/cell_panel.js b/object_database/web/devtools/cell_panel.js new file mode 100644 index 000000000..3f0a465e4 --- /dev/null +++ b/object_database/web/devtools/cell_panel.js @@ -0,0 +1 @@ +document.querySelector("div#main").textContent = "main found"; diff --git a/object_database/web/devtools/cells_panel.css b/object_database/web/devtools/cells_panel.css new file mode 100644 index 000000000..711c4296f --- /dev/null +++ b/object_database/web/devtools/cells_panel.css @@ -0,0 +1,4 @@ +div#main { + display: flex; + justify-content: center; +} diff --git a/object_database/web/devtools/cells_panel.html b/object_database/web/devtools/cells_panel.html new file mode 100644 index 000000000..597cae418 --- /dev/null +++ b/object_database/web/devtools/cells_panel.html @@ -0,0 +1,12 @@ + + + + + Cells + + + +
OK
+ + + diff --git a/object_database/web/devtools/devtools.html b/object_database/web/devtools/devtools.html new file mode 100644 index 000000000..717330a16 --- /dev/null +++ b/object_database/web/devtools/devtools.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/object_database/web/devtools/devtools_init.js b/object_database/web/devtools/devtools_init.js new file mode 100644 index 000000000..d814c986d --- /dev/null +++ b/object_database/web/devtools/devtools_init.js @@ -0,0 +1,10 @@ +// Create a new panel +chrome.devtools.panels.create( + "Cells", + null, + "cells_panel.html", + function(panel){ + console.log("ok"); + console.log(panel); + } +); diff --git a/object_database/web/devtools/index.html b/object_database/web/devtools/index.html new file mode 100644 index 000000000..02ef5dabc --- /dev/null +++ b/object_database/web/devtools/index.html @@ -0,0 +1,11 @@ + + + + +Devtools Test Page + + + +
Something about devtools
+ + diff --git a/object_database/web/devtools/manifest.json b/object_database/web/devtools/manifest.json new file mode 100644 index 000000000..0871ca7d7 --- /dev/null +++ b/object_database/web/devtools/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "cells devtools", + "version": "1.0", + "manifest_version": 2, + "devtools_page": "devtools.html" +} From f0a6e78caf521d9389675f5545c76527d0230d80 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sun, 2 Oct 2022 19:14:24 +0200 Subject: [PATCH 02/57] adding d3 tree viewer to the devtools cells panel (currently with fake data) --- object_database/web/devtools/cell_panel.js | 226 +++++++++++++++++- object_database/web/devtools/cells_panel.css | 21 ++ object_database/web/devtools/cells_panel.html | 3 +- object_database/web/devtools/manifest.json | 3 +- 4 files changed, 250 insertions(+), 3 deletions(-) diff --git a/object_database/web/devtools/cell_panel.js b/object_database/web/devtools/cell_panel.js index 3f0a465e4..b82edd5a5 100644 --- a/object_database/web/devtools/cell_panel.js +++ b/object_database/web/devtools/cell_panel.js @@ -1 +1,225 @@ -document.querySelector("div#main").textContent = "main found"; +const margin = { + top: 20, + right: 120, + bottom: 20, + left: 120 +}, +// width = 960 - margin.right - margin.left, +height = 800 - margin.top - margin.bottom; + +// SOME FAKE DATA: TODO! +const cells = { + name: "root cell", + children: [ + { + name: "cell_1_1", + children: [ + { + name: "cell_1_2", + children: [] + } + ] + }, + { + name: "cell_2_1", + children: [ + { + name: "cell_2_2", + children: [] + } + ] + }, + { + name: "cell_2_1", + children: [ + { + name: "cell_2_2", + children: [] + } + ] + }, + ] +} + +let i = 0, + duration = 750, + rectW = 60, + rectH = 30; + +const tree = d3.layout.tree().nodeSize([70, 40]); +const diagonal = d3.svg.diagonal() + .projection(function (d) { + return [d.x + rectW / 2, d.y + rectH / 2]; +}); + +const svg = d3.select("#main").append("svg").attr("width", "100%").attr("height", 1000) + .call(zm = d3.behavior.zoom().scaleExtent([1,3]).on("zoom", redraw)).append("g") + .attr("transform", "translate(" + 350 + "," + 20 + ")"); + +//necessary so that zoom knows where to zoom and unzoom from +zm.translate([350, 20]); + +cells.x0 = 0; +cells.y0 = height / 2; + +function collapse(d) { + if (d.children) { + d._children = d.children; + d._children.forEach(collapse); + d.children = null; + } +} + +cells.children.forEach(collapse); +update(cells); + +d3.select("#main").style("height", "400px"); + +function update(source) { + + // Compute the new tree layout. + const nodes = tree.nodes(cells).reverse(), + links = tree.links(nodes); + + // Normalize for fixed-depth. + nodes.forEach(function (d) { + d.y = d.depth * 180; + }); + + // Update the nodes… + const node = svg.selectAll("g.node") + .data(nodes, function (d) { + return d.id || (d.id = ++i); + }); + + // Enter any new nodes at the parent's previous position. + const nodeEnter = node.enter().append("g") + .attr("class", "node") + .attr("transform", function (d) { + return "translate(" + source.x0 + "," + source.y0 + ")"; + }) + .on("click", click); + + nodeEnter.append("rect") + .attr("width", rectW) + .attr("height", rectH) + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("fill", function (d) { + return d._children ? "lightsteelblue" : "#fff"; + }); + + nodeEnter.append("text") + .attr("x", rectW / 2) + .attr("y", rectH / 2) + .attr("dy", ".35em") + .attr("text-anchor", "middle") + .text(function (d) { + return d.name; + }); + + // Transition nodes to their new position. + const nodeUpdate = node.transition() + .duration(duration) + .attr("transform", function (d) { + return "translate(" + d.x + "," + d.y + ")"; + }); + + nodeUpdate.select("rect") + .attr("width", rectW) + .attr("height", rectH) + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("fill", function (d) { + return d._children ? "lightsteelblue" : "#fff"; + }); + + nodeUpdate.select("text") + .style("fill-opacity", 1); + + // Transition exiting nodes to the parent's new position. + const nodeExit = node.exit().transition() + .duration(duration) + .attr("transform", function (d) { + return "translate(" + source.x + "," + source.y + ")"; + }) + .remove(); + + nodeExit.select("rect") + .attr("width", rectW) + .attr("height", rectH) + //.attr("width", bbox.getBBox().width)"" + //.attr("height", bbox.getBBox().height) + .attr("stroke", "black") + .attr("stroke-width", 1); + + nodeExit.select("text"); + + // Update the links… + const link = svg.selectAll("path.link") + .data(links, function (d) { + return d.target.id; + }); + + // Enter any new links at the parent's previous position. + link.enter().insert("path", "g") + .attr("class", "link") + .attr("x", rectW / 2) + .attr("y", rectH / 2) + .attr("d", function (d) { + const o = { + x: source.x0, + y: source.y0 + }; + return diagonal({ + source: o, + target: o + }); + }); + + // Transition links to their new position. + link.transition() + .duration(duration) + .attr("d", diagonal); + + // Transition exiting nodes to the parent's new position. + link.exit().transition() + .duration(duration) + .attr("d", function (d) { + const o = { + x: source.x, + y: source.y + }; + return diagonal({ + source: o, + target: o + }); + }) + .remove(); + + // Stash the old positions for transition. + nodes.forEach(function (d) { + d.x0 = d.x; + d.y0 = d.y; + }); +} + +// Toggle children on click. +function click(d) { + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } + update(d); +} + +//Redraw for zoom +function redraw() { + //console.log("here", d3.event.translate, d3.event.scale); + svg.attr("transform", + "translate(" + d3.event.translate + ")" + + " scale(" + d3.event.scale + ")"); +} diff --git a/object_database/web/devtools/cells_panel.css b/object_database/web/devtools/cells_panel.css index 711c4296f..69d41b55e 100644 --- a/object_database/web/devtools/cells_panel.css +++ b/object_database/web/devtools/cells_panel.css @@ -2,3 +2,24 @@ div#main { display: flex; justify-content: center; } + +.node { + cursor: pointer; +} +.node circle { + fill: #fff; + stroke: steelblue; + stroke-width: 1.5px; +} +.node text { + font: 10px sans-serif; +} +.link { + fill: none; + stroke: #ccc; + stroke-width: 1.5px; +} + +body { + overflow: hidden; +} diff --git a/object_database/web/devtools/cells_panel.html b/object_database/web/devtools/cells_panel.html index 597cae418..b4c5762e3 100644 --- a/object_database/web/devtools/cells_panel.html +++ b/object_database/web/devtools/cells_panel.html @@ -3,10 +3,11 @@ Cells + -
OK
+
diff --git a/object_database/web/devtools/manifest.json b/object_database/web/devtools/manifest.json index 0871ca7d7..267cbb26a 100644 --- a/object_database/web/devtools/manifest.json +++ b/object_database/web/devtools/manifest.json @@ -2,5 +2,6 @@ "name": "cells devtools", "version": "1.0", "manifest_version": 2, - "devtools_page": "devtools.html" + "devtools_page": "devtools.html", + "content_security_policy": "script-src 'self' https://d3js.org; object-src 'self'" } From a4333ee923715a62ac3d5d8623392b7ac6198cfe Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 3 Oct 2022 21:44:16 +0200 Subject: [PATCH 03/57] initial structuring of the d3 code into something reasonable --- object_database/web/devtools/cell_panel.js | 248 ++++++++++++++++++- object_database/web/devtools/cells_panel.css | 1 + 2 files changed, 241 insertions(+), 8 deletions(-) diff --git a/object_database/web/devtools/cell_panel.js b/object_database/web/devtools/cell_panel.js index b82edd5a5..6d1650222 100644 --- a/object_database/web/devtools/cell_panel.js +++ b/object_database/web/devtools/cell_panel.js @@ -30,10 +30,10 @@ const cells = { ] }, { - name: "cell_2_1", + name: "cell_3_1", children: [ { - name: "cell_2_2", + name: "cell_3_2", children: [] } ] @@ -41,6 +41,233 @@ const cells = { ] } +class CellsTree extends Object { + constructor(data) { + super(); + + this.data = data; + + // basic view settings + // TODO: maybe these should be passed as params to the constructor + this.duration = 750; + this.rectW = 60; + this.rectH=30; + + // node ids + // increment for now + this.id = 0; + + // bound methods + this.setupTree = this.setupTree.bind(this); + this.update = this.update.bind(this); + this.collapse = this.collapse.bind(this); + this.redraw = this.redraw.bind(this); + this.onDblclick = this.onDblclick.bind(this); + } + + setupTree(){ + this.tree = d3.layout.tree().nodeSize([70, 40]); + const zm = d3.behavior.zoom() + .scaleExtent([1,3]) + .on("zoom", this.redraw) + // the main svg container for the tree + this.svg = d3.select("#main").append("svg") + .attr("width", "100%") + .attr("height", 1000) + .call(zm) + .append("g") + .attr( + "transform", + `translate(${350},${20})` + ); + //necessary so that zoom knows where to zoom and unzoom from + zm.translate([350, 20]); + + this.data.x0 = 0; + this.data.y0 = height / 2; + + // collapse all children for now + // TODO do we want this? + this.data.children.forEach(this.collapse); + + // build the tree + this.update(this.data); + } + + collapse(d) { + if (d.children) { + d._children = d.children; + d._children.forEach(this.collapse); + d.children = null; + } + } + + update(data) { + + const rectW = this.rectW; + const rectH = this.rectH; + + const diagonal = d3.svg.diagonal() + .projection(function (d) { + return [d.x + rectW / 2, d.y + rectH / 2]; + }); + + // Compute the new tree layout. + const nodes = this.tree.nodes(this.data) + .reverse() + + const links = this.tree.links(nodes); + + // Normalize for fixed-depth. + nodes.forEach(function (d) { + d.y = d.depth * 180; + }); + + // Update the nodes… + const node = this.svg.selectAll("g.node") + .data(nodes, function (d) { + console.log(d); + return d.id || (d.id = ++this.id); + }); + + // Enter any new nodes at the parent's previous position. + const nodeEnter = node.enter().append("g") + .attr("class", "node") + .attr("transform", function (d) { + return `translate(${data.x0},${data.y0})`; + }) + .on("dblclick", this.onDblclick); + + nodeEnter.append("rect") + .attr("width", rectW) + .attr("height", rectH) + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("fill", function (d) { + return d._children ? "lightsteelblue" : "#fff"; + }); + + nodeEnter.append("text") + .attr("x", rectW / 2) + .attr("y", rectH / 2) + .attr("dy", ".35em") + .attr("text-anchor", "middle") + .text(function (d) { + return d.name; + }); + + // Transition nodes to their new position. + const nodeUpdate = node.transition() + .duration(this.duration) + .attr("transform", function (d) { + return `translate(${d.x},${d.y})`; + }); + + nodeUpdate.select("rect") + .attr("width", rectW) + .attr("height", rectH) + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("fill", function (d) { + return d._children ? "lightsteelblue" : "#fff"; + }); + + nodeUpdate.select("text") + .style("fill-opacity", 1); + + // Transition exiting nodes to the parent's new position. + const nodeExit = node.exit().transition() + .duration(this.duration) + .attr("transform", function (d) { + return `translate(${data.x},${data.y})`; + }) + .remove(); + + nodeExit.select("rect") + .attr("width", rectW) + .attr("height", rectH) + .attr("stroke", "black") + .attr("stroke-width", 1); + + nodeExit.select("text"); + + // Update the links… + const link = this.svg.selectAll("path.link") + .data(links, function (d) { + return d.target.id; + }); + + // Enter any new links at the parent's previous position. + link.enter().insert("path", "g") + .attr("class", "link") + .attr("x", rectW / 2) + .attr("y", rectH / 2) + .attr("d", function (d) { + const o = { + x: data.x0, + y: data.y0 + }; + return diagonal({ + source: o, + target: o + }); + }); + + // Transition links to their new position. + link.transition() + .duration(this.duration) + .attr("d", diagonal); + + // Transition exiting nodes to the parent's new position. + link.exit().transition() + .duration(this.duration) + .attr("d", function (d) { + const o = { + x: data.x, + y: data.y + }; + return diagonal({ + source: o, + target: o + }); + }) + .remove(); + + // Stash the old positions for transition. + nodes.forEach(function (d) { + d.x0 = d.x; + d.y0 = d.y; + }); + } + + // Toggle children on click. + onDblclick(d) { + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } + this.update(d); + // prevent the default zooming in/out behavior + d3.event.stopPropagation(); + } + + //Redraw for zoom + redraw() { + this.svg.attr("transform", + `translate(${d3.event.translate})` + +`scale(${d3.event.scale})` + ); + } +} + + +// init and run +// const cTree = new CellsTree(cells); +// cTree.setupTree(); + let i = 0, duration = 750, rectW = 60, @@ -52,9 +279,10 @@ const diagonal = d3.svg.diagonal() return [d.x + rectW / 2, d.y + rectH / 2]; }); +const zm = d3.behavior.zoom().scaleExtent([1,3]); const svg = d3.select("#main").append("svg").attr("width", "100%").attr("height", 1000) - .call(zm = d3.behavior.zoom().scaleExtent([1,3]).on("zoom", redraw)).append("g") - .attr("transform", "translate(" + 350 + "," + 20 + ")"); + .call(zm).on("zoom", redraw).append("g") + .attr("transform", "translate(" + 350 + "," + 20 + ")"); //necessary so that zoom knows where to zoom and unzoom from zm.translate([350, 20]); @@ -89,6 +317,8 @@ function update(source) { // Update the nodes… const node = svg.selectAll("g.node") .data(nodes, function (d) { + console.log(d) + console.log(d.id) return d.id || (d.id = ++i); }); @@ -96,9 +326,9 @@ function update(source) { const nodeEnter = node.enter().append("g") .attr("class", "node") .attr("transform", function (d) { - return "translate(" + source.x0 + "," + source.y0 + ")"; - }) - .on("click", click); + return `translate(${source.x0},${source.y0})`; + }) + .on("dblclick", onDblclick); nodeEnter.append("rect") .attr("width", rectW) @@ -205,7 +435,7 @@ function update(source) { } // Toggle children on click. -function click(d) { +function onDblclick(d) { if (d.children) { d._children = d.children; d.children = null; @@ -214,6 +444,8 @@ function click(d) { d._children = null; } update(d); + // prevent the default zooming in/out behavior + d3.event.stopPropagation(); } //Redraw for zoom diff --git a/object_database/web/devtools/cells_panel.css b/object_database/web/devtools/cells_panel.css index 69d41b55e..0a78d0334 100644 --- a/object_database/web/devtools/cells_panel.css +++ b/object_database/web/devtools/cells_panel.css @@ -1,6 +1,7 @@ div#main { display: flex; justify-content: center; + height: 400px; } .node { From d11bf6721df54e0a343c4e6258e97231cf032d39 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 10 Oct 2022 13:02:49 +0200 Subject: [PATCH 04/57] adding info split-panel for cells organizing code into module updating some css --- object_database/web/devtools/cell_panel.js | 457 ------------------ object_database/web/devtools/cells_panel.css | 21 +- object_database/web/devtools/cells_panel.html | 3 +- object_database/web/devtools/js/cell_panel.js | 39 ++ object_database/web/devtools/js/tree.js | 238 +++++++++ 5 files changed, 299 insertions(+), 459 deletions(-) delete mode 100644 object_database/web/devtools/cell_panel.js create mode 100644 object_database/web/devtools/js/cell_panel.js create mode 100644 object_database/web/devtools/js/tree.js diff --git a/object_database/web/devtools/cell_panel.js b/object_database/web/devtools/cell_panel.js deleted file mode 100644 index 6d1650222..000000000 --- a/object_database/web/devtools/cell_panel.js +++ /dev/null @@ -1,457 +0,0 @@ -const margin = { - top: 20, - right: 120, - bottom: 20, - left: 120 -}, -// width = 960 - margin.right - margin.left, -height = 800 - margin.top - margin.bottom; - -// SOME FAKE DATA: TODO! -const cells = { - name: "root cell", - children: [ - { - name: "cell_1_1", - children: [ - { - name: "cell_1_2", - children: [] - } - ] - }, - { - name: "cell_2_1", - children: [ - { - name: "cell_2_2", - children: [] - } - ] - }, - { - name: "cell_3_1", - children: [ - { - name: "cell_3_2", - children: [] - } - ] - }, - ] -} - -class CellsTree extends Object { - constructor(data) { - super(); - - this.data = data; - - // basic view settings - // TODO: maybe these should be passed as params to the constructor - this.duration = 750; - this.rectW = 60; - this.rectH=30; - - // node ids - // increment for now - this.id = 0; - - // bound methods - this.setupTree = this.setupTree.bind(this); - this.update = this.update.bind(this); - this.collapse = this.collapse.bind(this); - this.redraw = this.redraw.bind(this); - this.onDblclick = this.onDblclick.bind(this); - } - - setupTree(){ - this.tree = d3.layout.tree().nodeSize([70, 40]); - const zm = d3.behavior.zoom() - .scaleExtent([1,3]) - .on("zoom", this.redraw) - // the main svg container for the tree - this.svg = d3.select("#main").append("svg") - .attr("width", "100%") - .attr("height", 1000) - .call(zm) - .append("g") - .attr( - "transform", - `translate(${350},${20})` - ); - //necessary so that zoom knows where to zoom and unzoom from - zm.translate([350, 20]); - - this.data.x0 = 0; - this.data.y0 = height / 2; - - // collapse all children for now - // TODO do we want this? - this.data.children.forEach(this.collapse); - - // build the tree - this.update(this.data); - } - - collapse(d) { - if (d.children) { - d._children = d.children; - d._children.forEach(this.collapse); - d.children = null; - } - } - - update(data) { - - const rectW = this.rectW; - const rectH = this.rectH; - - const diagonal = d3.svg.diagonal() - .projection(function (d) { - return [d.x + rectW / 2, d.y + rectH / 2]; - }); - - // Compute the new tree layout. - const nodes = this.tree.nodes(this.data) - .reverse() - - const links = this.tree.links(nodes); - - // Normalize for fixed-depth. - nodes.forEach(function (d) { - d.y = d.depth * 180; - }); - - // Update the nodes… - const node = this.svg.selectAll("g.node") - .data(nodes, function (d) { - console.log(d); - return d.id || (d.id = ++this.id); - }); - - // Enter any new nodes at the parent's previous position. - const nodeEnter = node.enter().append("g") - .attr("class", "node") - .attr("transform", function (d) { - return `translate(${data.x0},${data.y0})`; - }) - .on("dblclick", this.onDblclick); - - nodeEnter.append("rect") - .attr("width", rectW) - .attr("height", rectH) - .attr("stroke", "black") - .attr("stroke-width", 1) - .style("fill", function (d) { - return d._children ? "lightsteelblue" : "#fff"; - }); - - nodeEnter.append("text") - .attr("x", rectW / 2) - .attr("y", rectH / 2) - .attr("dy", ".35em") - .attr("text-anchor", "middle") - .text(function (d) { - return d.name; - }); - - // Transition nodes to their new position. - const nodeUpdate = node.transition() - .duration(this.duration) - .attr("transform", function (d) { - return `translate(${d.x},${d.y})`; - }); - - nodeUpdate.select("rect") - .attr("width", rectW) - .attr("height", rectH) - .attr("stroke", "black") - .attr("stroke-width", 1) - .style("fill", function (d) { - return d._children ? "lightsteelblue" : "#fff"; - }); - - nodeUpdate.select("text") - .style("fill-opacity", 1); - - // Transition exiting nodes to the parent's new position. - const nodeExit = node.exit().transition() - .duration(this.duration) - .attr("transform", function (d) { - return `translate(${data.x},${data.y})`; - }) - .remove(); - - nodeExit.select("rect") - .attr("width", rectW) - .attr("height", rectH) - .attr("stroke", "black") - .attr("stroke-width", 1); - - nodeExit.select("text"); - - // Update the links… - const link = this.svg.selectAll("path.link") - .data(links, function (d) { - return d.target.id; - }); - - // Enter any new links at the parent's previous position. - link.enter().insert("path", "g") - .attr("class", "link") - .attr("x", rectW / 2) - .attr("y", rectH / 2) - .attr("d", function (d) { - const o = { - x: data.x0, - y: data.y0 - }; - return diagonal({ - source: o, - target: o - }); - }); - - // Transition links to their new position. - link.transition() - .duration(this.duration) - .attr("d", diagonal); - - // Transition exiting nodes to the parent's new position. - link.exit().transition() - .duration(this.duration) - .attr("d", function (d) { - const o = { - x: data.x, - y: data.y - }; - return diagonal({ - source: o, - target: o - }); - }) - .remove(); - - // Stash the old positions for transition. - nodes.forEach(function (d) { - d.x0 = d.x; - d.y0 = d.y; - }); - } - - // Toggle children on click. - onDblclick(d) { - if (d.children) { - d._children = d.children; - d.children = null; - } else { - d.children = d._children; - d._children = null; - } - this.update(d); - // prevent the default zooming in/out behavior - d3.event.stopPropagation(); - } - - //Redraw for zoom - redraw() { - this.svg.attr("transform", - `translate(${d3.event.translate})` - +`scale(${d3.event.scale})` - ); - } -} - - -// init and run -// const cTree = new CellsTree(cells); -// cTree.setupTree(); - -let i = 0, - duration = 750, - rectW = 60, - rectH = 30; - -const tree = d3.layout.tree().nodeSize([70, 40]); -const diagonal = d3.svg.diagonal() - .projection(function (d) { - return [d.x + rectW / 2, d.y + rectH / 2]; -}); - -const zm = d3.behavior.zoom().scaleExtent([1,3]); -const svg = d3.select("#main").append("svg").attr("width", "100%").attr("height", 1000) - .call(zm).on("zoom", redraw).append("g") - .attr("transform", "translate(" + 350 + "," + 20 + ")"); - -//necessary so that zoom knows where to zoom and unzoom from -zm.translate([350, 20]); - -cells.x0 = 0; -cells.y0 = height / 2; - -function collapse(d) { - if (d.children) { - d._children = d.children; - d._children.forEach(collapse); - d.children = null; - } -} - -cells.children.forEach(collapse); -update(cells); - -d3.select("#main").style("height", "400px"); - -function update(source) { - - // Compute the new tree layout. - const nodes = tree.nodes(cells).reverse(), - links = tree.links(nodes); - - // Normalize for fixed-depth. - nodes.forEach(function (d) { - d.y = d.depth * 180; - }); - - // Update the nodes… - const node = svg.selectAll("g.node") - .data(nodes, function (d) { - console.log(d) - console.log(d.id) - return d.id || (d.id = ++i); - }); - - // Enter any new nodes at the parent's previous position. - const nodeEnter = node.enter().append("g") - .attr("class", "node") - .attr("transform", function (d) { - return `translate(${source.x0},${source.y0})`; - }) - .on("dblclick", onDblclick); - - nodeEnter.append("rect") - .attr("width", rectW) - .attr("height", rectH) - .attr("stroke", "black") - .attr("stroke-width", 1) - .style("fill", function (d) { - return d._children ? "lightsteelblue" : "#fff"; - }); - - nodeEnter.append("text") - .attr("x", rectW / 2) - .attr("y", rectH / 2) - .attr("dy", ".35em") - .attr("text-anchor", "middle") - .text(function (d) { - return d.name; - }); - - // Transition nodes to their new position. - const nodeUpdate = node.transition() - .duration(duration) - .attr("transform", function (d) { - return "translate(" + d.x + "," + d.y + ")"; - }); - - nodeUpdate.select("rect") - .attr("width", rectW) - .attr("height", rectH) - .attr("stroke", "black") - .attr("stroke-width", 1) - .style("fill", function (d) { - return d._children ? "lightsteelblue" : "#fff"; - }); - - nodeUpdate.select("text") - .style("fill-opacity", 1); - - // Transition exiting nodes to the parent's new position. - const nodeExit = node.exit().transition() - .duration(duration) - .attr("transform", function (d) { - return "translate(" + source.x + "," + source.y + ")"; - }) - .remove(); - - nodeExit.select("rect") - .attr("width", rectW) - .attr("height", rectH) - //.attr("width", bbox.getBBox().width)"" - //.attr("height", bbox.getBBox().height) - .attr("stroke", "black") - .attr("stroke-width", 1); - - nodeExit.select("text"); - - // Update the links… - const link = svg.selectAll("path.link") - .data(links, function (d) { - return d.target.id; - }); - - // Enter any new links at the parent's previous position. - link.enter().insert("path", "g") - .attr("class", "link") - .attr("x", rectW / 2) - .attr("y", rectH / 2) - .attr("d", function (d) { - const o = { - x: source.x0, - y: source.y0 - }; - return diagonal({ - source: o, - target: o - }); - }); - - // Transition links to their new position. - link.transition() - .duration(duration) - .attr("d", diagonal); - - // Transition exiting nodes to the parent's new position. - link.exit().transition() - .duration(duration) - .attr("d", function (d) { - const o = { - x: source.x, - y: source.y - }; - return diagonal({ - source: o, - target: o - }); - }) - .remove(); - - // Stash the old positions for transition. - nodes.forEach(function (d) { - d.x0 = d.x; - d.y0 = d.y; - }); -} - -// Toggle children on click. -function onDblclick(d) { - if (d.children) { - d._children = d.children; - d.children = null; - } else { - d.children = d._children; - d._children = null; - } - update(d); - // prevent the default zooming in/out behavior - d3.event.stopPropagation(); -} - -//Redraw for zoom -function redraw() { - //console.log("here", d3.event.translate, d3.event.scale); - svg.attr("transform", - "translate(" + d3.event.translate + ")" - + " scale(" + d3.event.scale + ")"); -} diff --git a/object_database/web/devtools/cells_panel.css b/object_database/web/devtools/cells_panel.css index 0a78d0334..8c5ad5dac 100644 --- a/object_database/web/devtools/cells_panel.css +++ b/object_database/web/devtools/cells_panel.css @@ -1,7 +1,26 @@ +html { + height: 100% !important; +} + +body { + height: 100% !important; + display: flex; + flex-direction: row; + margin: 0px; +} + div#main { display: flex; justify-content: center; - height: 400px; + min-width: 70%; + border-right: solid 2px; +} + +div#cell-info { + display: flex; + justify-content: center; + align-items: center; + min-width: 30%; } .node { diff --git a/object_database/web/devtools/cells_panel.html b/object_database/web/devtools/cells_panel.html index b4c5762e3..a89140912 100644 --- a/object_database/web/devtools/cells_panel.html +++ b/object_database/web/devtools/cells_panel.html @@ -8,6 +8,7 @@
+
some cells data here
- + diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js new file mode 100644 index 000000000..54736f62e --- /dev/null +++ b/object_database/web/devtools/js/cell_panel.js @@ -0,0 +1,39 @@ +import {CellsTree} from './tree.js'; + +// SOME FAKE DATA: TODO! +const cells = { + name: "root cell", + children: [ + { + name: "cell_1_1", + children: [ + { + name: "cell_1_2", + children: [] + } + ] + }, + { + name: "cell_2_1", + children: [ + { + name: "cell_2_2", + children: [] + } + ] + }, + { + name: "cell_3_1", + children: [ + { + name: "cell_3_2", + children: [] + } + ] + }, + ] +} + +// init and run +const cTree = new CellsTree(cells); +cTree.setupTree(); diff --git a/object_database/web/devtools/js/tree.js b/object_database/web/devtools/js/tree.js new file mode 100644 index 000000000..c5101d0d2 --- /dev/null +++ b/object_database/web/devtools/js/tree.js @@ -0,0 +1,238 @@ + +class CellsTree extends Object { + constructor(data) { + super(); + + this.data = data; + + // basic view settings + // TODO: maybe these should be passed as params to the constructor + this.duration = 750; + this.rectW = 60; + this.rectH=30; + + // bound methods + this.setupTree = this.setupTree.bind(this); + this.update = this.update.bind(this); + this.collapse = this.collapse.bind(this); + this.redraw = this.redraw.bind(this); + this.onDblclick = this.onDblclick.bind(this); + this.onClick = this.onClick.bind(this); + } + + setupTree(){ + this.tree = d3.layout.tree().nodeSize([70, 40]); + const zm = d3.behavior.zoom() + .scaleExtent([1,3]) + .on("zoom", this.redraw) + // the main svg container for the tree + this.svg = d3.select("#main").append("svg") + .attr("width", "100%") + .attr("height", 1000) + .call(zm) + .append("g") + .attr( + "transform", + `translate(${350},${20})` + ); + //necessary so that zoom knows where to zoom and unzoom from + zm.translate([350, 20]); + + this.data.x0 = 0; + this.data.y0 = 700 / 2; + + // collapse all children for now + // TODO do we want this? + // this.data.children.forEach(this.collapse); + + // build the tree + this.update(this.data); + } + + update(source) { + // need to define these in method scope since a number of the + // callbacks are not bound to the class. TODO + let id = 0; + const rectW = this.rectW; + const rectH = this.rectH; + + const diagonal = d3.svg.diagonal() + .projection(function (d) { + return [d.x + rectW / 2, d.y + rectH / 2]; + }); + // Compute the new tree layout. + const nodes = this.tree.nodes(this.data).reverse(); + const links = this.tree.links(nodes); + + // Normalize for fixed-depth. + nodes.forEach(function (d) { + d.y = d.depth * 180; + }); + + // Update the nodes… + const node = this.svg.selectAll("g.node") + .data(nodes, function (d) { + return d.id || (d.id = ++id); + } + ); + + // Enter any new nodes at the parent's previous position. + const nodeEnter = node.enter().append("g") + .attr("class", "node") + .attr("transform", function (d) { + return `translate(${source.x0},${source.y0})`; + }) + .on("dblclick", this.onDblclick) + .on("click", this.onClick); + + nodeEnter.append("rect") + .attr("width", this.rectW) + .attr("height", this.rectH) + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("fill", function (d) { + return d._children ? "lightsteelblue" : "#fff"; + } + ); + + nodeEnter.append("text") + .attr("x", this.rectW / 2) + .attr("y", this.rectH / 2) + .attr("dy", ".35em") + .attr("text-anchor", "middle") + .text(function (d) { + return d.name; + } + ); + + // Transition nodes to their new position. + const nodeUpdate = node.transition() + .duration(this.duration) + .attr("transform", function (d) { + return `translate(${d.x},${d.y})`; + } + ); + + nodeUpdate.select("rect") + .attr("width", this.rectW) + .attr("height", this.rectH) + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("fill", function (d) { + return d._children ? "lightsteelblue" : "#fff"; + } + ); + + nodeUpdate.select("text") + .style("fill-opacity", 1); + + // Transition exiting nodes to the parent's new position. + const nodeExit = node.exit().transition() + .duration(this.duration) + .attr("transform", function (d) { + return `translate(${source.x},${source.y})`; + } + ).remove(); + + nodeExit.select("rect") + .attr("width", this.rectW) + .attr("height", this.rectH) + .attr("stroke", "black") + .attr("stroke-width", 1); + + nodeExit.select("text"); + + // Update the links… + const link = this.svg.selectAll("path.link") + .data(links, function (d) { + return d.target.id; + } + ); + + // Enter any new links at the parent's previous position. + link.enter().insert("path", "g") + .attr("class", "link") + .attr("x", this.rectW / 2) + .attr("y", this.rectH / 2) + .attr("d", function (d) { + const o = { + x: source.x0, + y: source.y0 + }; + return diagonal({ + source: o, + target: o + }); + }); + + // Transition links to their new position. + link.transition() + .duration(this.duration) + .attr("d", diagonal); + + // Transition exiting nodes to the parent's new position. + link.exit().transition() + .duration(this.duration) + .attr("d", function (d) { + const o = { + x: source.x, + y: source.y + }; + return diagonal({ + source: o, + target: o + }); + }) + .remove(); + + // Stash the old positions for transition. + nodes.forEach(function (d) { + d.x0 = d.x; + d.y0 = d.y; + }); + } + + collapse(d) { + if (d.children) { + d._children = d.children.slice(); + d._children.forEach(this.collapse); + d.children = null; + } + } + + + // Toggle children on click. + onDblclick(d) { + // prevent the default zooming in/out behavior + d3.event.stopPropagation(); + if (d.children) { + d._children = d.children.slice(); + d.children = null; + } else { + d.children = d._children.slice(); + d._children = null; + } + this.update(d); + } + + onClick(event){ + // update the cell data + // probably should be handled by a different class + const infoDiv = document.getElementById("cell-info") + infoDiv.textContent = event.name; + } + + //Redraw for zoom + redraw() { + this.svg.attr("transform", + `translate(${d3.event.translate})` + +`scale(${d3.event.scale})` + ); + } +} + + +export { + CellsTree, + CellsTree as default +} From 93cf81b926b908f4337fc783052d9611f1605ee8 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 10 Oct 2022 14:12:16 +0200 Subject: [PATCH 05/57] adding background and content-script js files --- object_database/web/devtools/js/background.js | 55 +++++++++++++++++++ .../web/devtools/js/content-script.js | 28 ++++++++++ object_database/web/devtools/manifest.json | 15 +++++ 3 files changed, 98 insertions(+) create mode 100644 object_database/web/devtools/js/background.js create mode 100644 object_database/web/devtools/js/content-script.js diff --git a/object_database/web/devtools/js/background.js b/object_database/web/devtools/js/background.js new file mode 100644 index 000000000..a626ecdb5 --- /dev/null +++ b/object_database/web/devtools/js/background.js @@ -0,0 +1,55 @@ +/* + * The background script handles communication to and from + * the content script, embedded in the document, and + * the panel scripts, living in devtools. + * These communiccation are handled by chrome.runtime connection + * ports. + * The connections are initialized int he content and panel scripts, + * respectively. Here we listen for these connection and create + * connection/port specific handlers. + */ +var portFromCS; +var portFromPanel; + +function connected(port) { + // handle all communication to and from the panel + if (port.name === "port-from-panel"){ + portFromPanel = port; + // at the moment we don't do anything with messages coming + // from the panels + portFromPanel.onMessage.addListener(function(msg) { + console.log("recieved message from panel", msg); + }); + }; + // handle all communication to and from the content script + if (port.name === "port-from-cs"){ + portFromCS = port; + portFromCS.onMessage.addListener(function(msg) { + // Having received a message from the content script, i.e. + // from the target window we forward this message to the panels + // if the connection is not alive we log this in the devtools's + // debugger console + notifyDevtoolsPabel(msg.data); + }); + } + // notify if the port has disconnected + port.onDisconnect.addListener(function(port) { + if (port.name === "port-from-panel" || port.name === "port-from-cs"){ + console.log(`${port.name}} has disconnected`); + }; + }); +} + +chrome.runtime.onConnect.addListener(connected); + +function notifyDevtoolsPabel(msg){ + if (portFromPanel){ + portFromPanel.postMessage(msg); + } else { + console.log("failed to send message to devtools panel: port disconnected"); + } +} + +// chrome.browserAction.onClicked.addListener(function() { +// portFromCS.postMessage({greeting: "they clicked the button!"}); +// }); diff --git a/object_database/web/devtools/js/content-script.js b/object_database/web/devtools/js/content-script.js new file mode 100644 index 000000000..ca1ad7424 --- /dev/null +++ b/object_database/web/devtools/js/content-script.js @@ -0,0 +1,28 @@ +/* + * I am the conect script which is injected into the target + * document window when devtools is open. + * + * I create connection port to handle communication between myself + * and the devtools browser script (which then passes these messages + * onto the devtools panel scripts). + * + * In addition, I handle incoming window level messaging ( + * window.postMessage() API) and routing these application + * originating messaged to the devtools background. + */ + +var portFromCS = chrome.runtime.connect({name:"port-from-cs"}); + +// at the moment nothing much is done with messages going +// to the content-script port +portFromCS.onMessage.addListener(function(msg) { + console.log("recieved message from background", msg); +}); + +window.addEventListener("message", (event) => { + // filter on the target windows url + if(event.origin === window.location.origin){ + // reoute the message to the background script + portFromCS.postMessage({data: event.data}); + } +}, false); diff --git a/object_database/web/devtools/manifest.json b/object_database/web/devtools/manifest.json index 267cbb26a..47a4b40ae 100644 --- a/object_database/web/devtools/manifest.json +++ b/object_database/web/devtools/manifest.json @@ -4,4 +4,19 @@ "manifest_version": 2, "devtools_page": "devtools.html", "content_security_policy": "script-src 'self' https://d3js.org; object-src 'self'" + "content_scripts": [ + { + "matches": [ + "*://*/*" + ], + "js": [ + "./js/content-script.js" + ] + } + ], + "background": { + "scripts": [ + "./js/background.js" + ] + }, } From 9eb8459cbca0219aefb8edb2f5aba86c62403ac0 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 10 Oct 2022 23:18:55 +0200 Subject: [PATCH 06/57] updating communication between background target and devtools issues with Manfiest v3 TODO --- .../web/devtools/{js => }/background.js | 3 +- .../web/devtools/{js => }/content-script.js | 7 ++- object_database/web/devtools/devtools_init.js | 46 ++++++++++++++++++- object_database/web/devtools/js/cell_panel.js | 7 +++ object_database/web/devtools/manifest.json | 8 ++-- 5 files changed, 62 insertions(+), 9 deletions(-) rename object_database/web/devtools/{js => }/background.js (95%) rename object_database/web/devtools/{js => }/content-script.js (81%) diff --git a/object_database/web/devtools/js/background.js b/object_database/web/devtools/background.js similarity index 95% rename from object_database/web/devtools/js/background.js rename to object_database/web/devtools/background.js index a626ecdb5..b97fc84c0 100644 --- a/object_database/web/devtools/js/background.js +++ b/object_database/web/devtools/background.js @@ -35,7 +35,7 @@ function connected(port) { // notify if the port has disconnected port.onDisconnect.addListener(function(port) { if (port.name === "port-from-panel" || port.name === "port-from-cs"){ - console.log(`${port.name}} has disconnected`); + console.log(`${port.name} has disconnected`); }; }); } @@ -46,6 +46,7 @@ function notifyDevtoolsPabel(msg){ if (portFromPanel){ portFromPanel.postMessage(msg); } else { + console.log(msg); console.log("failed to send message to devtools panel: port disconnected"); } } diff --git a/object_database/web/devtools/js/content-script.js b/object_database/web/devtools/content-script.js similarity index 81% rename from object_database/web/devtools/js/content-script.js rename to object_database/web/devtools/content-script.js index ca1ad7424..cd8e902fc 100644 --- a/object_database/web/devtools/js/content-script.js +++ b/object_database/web/devtools/content-script.js @@ -11,18 +11,21 @@ * originating messaged to the devtools background. */ + +console.log("loading content script"); var portFromCS = chrome.runtime.connect({name:"port-from-cs"}); // at the moment nothing much is done with messages going // to the content-script port portFromCS.onMessage.addListener(function(msg) { - console.log("recieved message from background", msg); + // console.log("received message from background"); }); window.addEventListener("message", (event) => { // filter on the target windows url + console.log("message in target window") if(event.origin === window.location.origin){ - // reoute the message to the background script + // reroute the message to the background script portFromCS.postMessage({data: event.data}); } }, false); diff --git a/object_database/web/devtools/devtools_init.js b/object_database/web/devtools/devtools_init.js index d814c986d..33043bbf3 100644 --- a/object_database/web/devtools/devtools_init.js +++ b/object_database/web/devtools/devtools_init.js @@ -4,7 +4,49 @@ chrome.devtools.panels.create( null, "cells_panel.html", function(panel){ - console.log("ok"); - console.log(panel); + let _window = null; // hold a reference to cell_panel.html + const data = []; + + // create a connection/port which will handle all communication + // between the panel and the background script + const portFromPanel = chrome.runtime.connect({name: "port-from-panel"}); + portFromPanel.onMessage.addListener(function(msg) { + if (_window){ + // handleMessageFromBackground() is defined in panel.js + // TODO _window.handleMessageFromBackground(msg); + //console.log("msg from background") + } else { + console.log("no connection to background"); + // if the panel's window is undefined store the data for now + data.push(msg); + } + }); + + // when the panel button is clicked + panel.onShown.addListener(function tmp(panelWindow) { + console.log("panel is being shown"); + // clean up any stale listeners + panel.onShown.removeListener(tmp); + + // set the _window const to panelWindow which allows handling + // of messages by the panel, i.e. in the panel's window context + _window = panelWindow; + const msg = null; + // if any data was logged while the panel was not available + // send it along now + /* + while (msg == data.shift()){ + console.log("msg from background") + // TODO _window.handleMessageFromBackground(msg); + } + */ + // If we ever need to send messages back via the port + // we can do that as below + _window.respond = function(msg) { + portFromPanel.postMessage(msg); + } + }); + + panel.onHidden.addListener(function() {console.log("panel is being hidden")}); console.log(panel); } ); diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 54736f62e..f7f3cbf9b 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -1,5 +1,12 @@ import {CellsTree} from './tree.js'; +// setup message handling from background +function handleMessageFromBackground(msg){ + console.log("handling background message"); + console.log(msg); +} + + // SOME FAKE DATA: TODO! const cells = { name: "root cell", diff --git a/object_database/web/devtools/manifest.json b/object_database/web/devtools/manifest.json index 47a4b40ae..27b9c1883 100644 --- a/object_database/web/devtools/manifest.json +++ b/object_database/web/devtools/manifest.json @@ -3,20 +3,20 @@ "version": "1.0", "manifest_version": 2, "devtools_page": "devtools.html", - "content_security_policy": "script-src 'self' https://d3js.org; object-src 'self'" + "content_security_policy": "script-src 'self' https://d3js.org; object-src 'self'", "content_scripts": [ { "matches": [ "*://*/*" ], "js": [ - "./js/content-script.js" + "content-script.js" ] } ], "background": { "scripts": [ - "./js/background.js" + "background.js" ] - }, + } } From 019b704b91e451af54badb0578c84aef2c44e58c Mon Sep 17 00:00:00 2001 From: dkrasner Date: Wed, 12 Oct 2022 12:21:08 +0200 Subject: [PATCH 07/57] adding a README with png and minor changes --- .../web/devtools/DevtoolsExtensions.png | Bin 0 -> 38470 bytes object_database/web/devtools/README.md | 71 ++++++++++++++++++ .../web/devtools/content-script.js | 2 +- object_database/web/devtools/devtools_init.js | 4 +- 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 object_database/web/devtools/DevtoolsExtensions.png create mode 100644 object_database/web/devtools/README.md diff --git a/object_database/web/devtools/DevtoolsExtensions.png b/object_database/web/devtools/DevtoolsExtensions.png new file mode 100644 index 0000000000000000000000000000000000000000..2fc5d466fbe746512bb04e3c5af881a2a01a1a73 GIT binary patch literal 38470 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 v-;T@zE7`g^*GpSx`p3ur+n Date: Sun, 16 Oct 2022 13:16:33 +0200 Subject: [PATCH 08/57] adding handleMessageFromBackground to devtools display initial load, reconnection displays setup for cells loadeded displays --- .../web/content/src/CellHandler.js | 17 ++++- object_database/web/devtools/cells_panel.css | 1 + .../web/devtools/content-script.js | 9 ++- object_database/web/devtools/devtools_init.js | 18 +++-- object_database/web/devtools/js/cell_panel.js | 68 ++++++++++++++++--- 5 files changed, 89 insertions(+), 24 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 7664f035c..a5d7202bb 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -58,6 +58,9 @@ class CellHandler { // the server-assigned session this.sessionId = null; + + // devtools related messages + this.sendMessageToDevtools = this.sendMessageToDevtools.bind(this); } tearDownAllLiveCells() { @@ -119,6 +122,12 @@ class CellHandler { ); } + sendMessageToDevtools(msg){ + // TODO perhaps this should be run by a worker + msg.type = "cells_devtools"; + window.postMessage(msg); + } + initialRender() { this.renderMainDiv( h("div", {class: 'container-fluid'}, [ @@ -127,6 +136,9 @@ class CellHandler { ]) ]) ); + this.sendMessageToDevtools({ + status: "initial load" + }); } afterConnected() { @@ -158,6 +170,9 @@ class CellHandler { ["Reconnecting in " + waitSeconds + " seconds"]) ]) ); + this.sendMessageToDevtools({ + status: "reconnecting" + }); } /** @@ -167,7 +182,7 @@ class CellHandler { * It will case out the appropriate handling * method based on the `type` field in the * message. - * Note taht we call `doesNotUnderstand()` + * Note that we call `doesNotUnderstand()` * in the event of a message containing a * message type that is unknown to the system. * @param {Object} message - A JSON decoded diff --git a/object_database/web/devtools/cells_panel.css b/object_database/web/devtools/cells_panel.css index 8c5ad5dac..eff198b16 100644 --- a/object_database/web/devtools/cells_panel.css +++ b/object_database/web/devtools/cells_panel.css @@ -14,6 +14,7 @@ div#main { justify-content: center; min-width: 70%; border-right: solid 2px; + align-items: center; } div#cell-info { diff --git a/object_database/web/devtools/content-script.js b/object_database/web/devtools/content-script.js index 13cdfa632..a1c02e588 100644 --- a/object_database/web/devtools/content-script.js +++ b/object_database/web/devtools/content-script.js @@ -23,9 +23,12 @@ portFromCS.onMessage.addListener(function(msg) { window.addEventListener("message", (event) => { // filter on the target windows url - console.log("message in target window") + // console.log("message in target window") if(event.origin === window.location.origin){ - // reroute the message to the background script - portFromCS.postMessage({data: event.data}); + // filter the message further to make sure it's for devtools + if(event.data.type == "cells_devtools"){ + // reroute the message to the background script + portFromCS.postMessage({data: event.data}); + } } }, false); diff --git a/object_database/web/devtools/devtools_init.js b/object_database/web/devtools/devtools_init.js index a12b1ae52..a9073b3f1 100644 --- a/object_database/web/devtools/devtools_init.js +++ b/object_database/web/devtools/devtools_init.js @@ -13,14 +13,13 @@ chrome.devtools.panels.create( portFromPanel.onMessage.addListener(function(msg) { if (_window){ // handleMessageFromBackground() is defined in panel.js - // TODO _window.handleMessageFromBackground(msg); - console.log("msg from background") - console.log(msg); + _window.handleMessageFromBackground(msg); } else { console.log("no connection to background"); // if the panel's window is undefined store the data for now data.push(msg); - console.log(`logged data: ${msg}`) + // console.log(`logged data:`) + // console.log(data); } }); @@ -33,15 +32,14 @@ chrome.devtools.panels.create( // set the _window const to panelWindow which allows handling // of messages by the panel, i.e. in the panel's window context _window = panelWindow; - const msg = null; + let msg = data.shift(); // if any data was logged while the panel was not available // send it along now - /* - while (msg == data.shift()){ - console.log("msg from background") - // TODO _window.handleMessageFromBackground(msg); + while (msg){ + // console.log("handling logged messages"); + _window.handleMessageFromBackground(msg); + msg = data.shift(); } - */ // If we ever need to send messages back via the port // we can do that as below _window.respond = function(msg) { diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index f7f3cbf9b..425b38488 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -1,12 +1,5 @@ import {CellsTree} from './tree.js'; -// setup message handling from background -function handleMessageFromBackground(msg){ - console.log("handling background message"); - console.log(msg); -} - - // SOME FAKE DATA: TODO! const cells = { name: "root cell", @@ -41,6 +34,61 @@ const cells = { ] } -// init and run -const cTree = new CellsTree(cells); -cTree.setupTree(); + +// GLOBALS (TODO: should be handled better) +let state = null; + + +// setup message handling from background +function handleMessageFromBackground(msg){ + // console.log("handling background message"); + // console.log(msg); + switch (msg.status){ + case "initial load": + state = msg.status; + initialLoadDisplay(); + break; + case "reconnecting": + // no need to redisplay reconnection attemps + if(state != msg.status){ + state = msg.status; + reconnectingDisplay(); + } + break; + case "loaded": + state = msg.status; + cellsTreeDisplay(); + } +} + +window.handleMessageFromBackground = handleMessageFromBackground; + + +const initialLoadDisplay = () => { + const main = document.getElementById("main"); + main.textContent = "Initializing: no cells loaded"; +} + +const reconnectingDisplay = () => { + const main = document.getElementById("main"); + main.textContent = "Reconnecting: no cells loaded"; +} + +const cellsTreeDisplay = () => { + clearDisplay(); + // init and run + // NOTE: the tree class itself attaches the + // svg element to #main + const cTree = new CellsTree(cells); + cTree.setupTree(); +} + + +/** + * I clear the views when the application + * views when the application state changes + **/ +const clearDisplay = () => { + document.getElementById("main").replaceChildren(); + document.getElementById("cell-info").replaceChildren(); +} From 2023e27260918433da1c6f3d3f9c5192a81633e8 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sun, 16 Oct 2022 15:29:29 +0200 Subject: [PATCH 09/57] first pass at getting tree data to debugger --- .../web/content/src/CellHandler.js | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index a5d7202bb..42afc8e6c 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -61,6 +61,7 @@ class CellHandler { // devtools related messages this.sendMessageToDevtools = this.sendMessageToDevtools.bind(this); + this.updateDevtools = this.updateDevtools.bind(this); } tearDownAllLiveCells() { @@ -122,12 +123,6 @@ class CellHandler { ); } - sendMessageToDevtools(msg){ - // TODO perhaps this should be run by a worker - msg.type = "cells_devtools"; - window.postMessage(msg); - } - initialRender() { this.renderMainDiv( h("div", {class: 'container-fluid'}, [ @@ -380,6 +375,7 @@ class CellHandler { + totalUpdateCount + " nodes created/updated." ) } + this.updateDevtools(); } } @@ -510,6 +506,33 @@ class CellHandler { this.socket.sendString(JSON.stringify(message)); } } + + sendMessageToDevtools(msg){ + // TODO perhaps this should be run by a worker + msg.type = "cells_devtools"; + window.postMessage(msg); + } + + /** + * Send updated cells data to devtools + **/ + updateDevtools(){ + const addToTree = (cell, parent) => { + if(!parent.children){ + parent.children = []; + } + parent.children.append({ + name: cell.constructor.name, + id: cell.identity, + children: Object.values(cell.namedChildren).map((child) => { + return addToTree(child, cell); + }) + }) + }; + const page_root = this.activeCells['page_root']; + const tree = addToTree(page_root, {}); + return tree; + } } export {CellHandler, CellHandler as default}; From 233bf5165724ec094c56e7ee3b0cc0334290c18d Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 17 Oct 2022 11:22:20 +0200 Subject: [PATCH 10/57] minor update to tree building for devtools --- object_database/web/content/src/CellHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 42afc8e6c..afe55271d 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -530,7 +530,7 @@ class CellHandler { }) }; const page_root = this.activeCells['page_root']; - const tree = addToTree(page_root, {}); + const tree = addToTree(page_root, {name: "PageRoot", id: "page_root", children: []}); return tree; } } From 0565251388ea1b13c5a22fb82215901f8b610a33 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 17 Oct 2022 10:52:08 +0000 Subject: [PATCH 11/57] wrapping up devtools cell tree data generator and msg method --- .../web/content/src/CellHandler.js | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index afe55271d..2e2fcf930 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -517,21 +517,37 @@ class CellHandler { * Send updated cells data to devtools **/ updateDevtools(){ - const addToTree = (cell, parent) => { - if(!parent.children){ - parent.children = []; - } - parent.children.append({ + const buildTree = (cell) => { + return { name: cell.constructor.name, id: cell.identity, - children: Object.values(cell.namedChildren).map((child) => { - return addToTree(child, cell); + children: mapChildren(cell.namedChildren) + } + } + // NOTE: sometimes a named child is a cell, sometimes it's an array of cells + // so we need a helper function to deal with these cases + const mapChildren = (namedChildren) => { + const children = []; + if (namedChildren) { + Object.values(namedChildren).forEach((child) => { + if (child){ + if (child instanceof Array){ + child.forEach((subchild) => { + children.push(buildTree(subchild)); + }) + } + children.push(buildTree(child)); + } }) - }) - }; + } + return children; + } const page_root = this.activeCells['page_root']; - const tree = addToTree(page_root, {name: "PageRoot", id: "page_root", children: []}); - return tree; + const tree = buildTree(page_root); + this.sendMessageToDevtools({ + status: "loaded", + cells: tree + }); } } From a23ff93537b7ce54ca3092dd2ebf7fd502d22f41 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 17 Oct 2022 14:41:07 +0200 Subject: [PATCH 12/57] adding mouseover and leave for cell tree nodes fixing up id class for d3 nodes and cells --- object_database/web/content/app.css | 5 +++ .../web/content/src/CellHandler.js | 2 +- object_database/web/devtools/js/cell_panel.js | 44 +++---------------- object_database/web/devtools/js/tree.js | 27 ++++++++++-- 4 files changed, 35 insertions(+), 43 deletions(-) diff --git a/object_database/web/content/app.css b/object_database/web/content/app.css index 1f500afe0..6b1c334a6 100644 --- a/object_database/web/content/app.css +++ b/object_database/web/content/app.css @@ -1096,3 +1096,8 @@ pre { overflow: auto; pointer-events: auto; } + +// devtools helpers +.devtools-inspect{ + background-color: lightblue!important; +} diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 2e2fcf930..a1f8b7cd5 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -520,7 +520,7 @@ class CellHandler { const buildTree = (cell) => { return { name: cell.constructor.name, - id: cell.identity, + identity: cell.identity, children: mapChildren(cell.namedChildren) } } diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 425b38488..fc5196de7 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -1,40 +1,5 @@ import {CellsTree} from './tree.js'; -// SOME FAKE DATA: TODO! -const cells = { - name: "root cell", - children: [ - { - name: "cell_1_1", - children: [ - { - name: "cell_1_2", - children: [] - } - ] - }, - { - name: "cell_2_1", - children: [ - { - name: "cell_2_2", - children: [] - } - ] - }, - { - name: "cell_3_1", - children: [ - { - name: "cell_3_2", - children: [] - } - ] - }, - ] -} - - // GLOBALS (TODO: should be handled better) let state = null; @@ -56,8 +21,11 @@ function handleMessageFromBackground(msg){ } break; case "loaded": - state = msg.status; - cellsTreeDisplay(); + if(state != msg.status){ + state = msg.status; + cellsTreeDisplay(msg.cells); + console.log(msg.cells); + } } } @@ -74,7 +42,7 @@ const reconnectingDisplay = () => { main.textContent = "Reconnecting: no cells loaded"; } -const cellsTreeDisplay = () => { +const cellsTreeDisplay = (cells) => { clearDisplay(); // init and run // NOTE: the tree class itself attaches the diff --git a/object_database/web/devtools/js/tree.js b/object_database/web/devtools/js/tree.js index c5101d0d2..81ec9db44 100644 --- a/object_database/web/devtools/js/tree.js +++ b/object_database/web/devtools/js/tree.js @@ -18,6 +18,8 @@ class CellsTree extends Object { this.redraw = this.redraw.bind(this); this.onDblclick = this.onDblclick.bind(this); this.onClick = this.onClick.bind(this); + this.onMouseover = this.onMouseover.bind(this); + this.onMouseleave = this.onMouseleave.bind(this); } setupTree(){ @@ -43,7 +45,7 @@ class CellsTree extends Object { // collapse all children for now // TODO do we want this? - // this.data.children.forEach(this.collapse); + this.data.children.forEach(this.collapse); // build the tree this.update(this.data); @@ -72,7 +74,7 @@ class CellsTree extends Object { // Update the nodes… const node = this.svg.selectAll("g.node") .data(nodes, function (d) { - return d.id || (d.id = ++id); + return d.id = ++id; } ); @@ -83,7 +85,10 @@ class CellsTree extends Object { return `translate(${source.x0},${source.y0})`; }) .on("dblclick", this.onDblclick) - .on("click", this.onClick); + .on("click", this.onClick) + .on("mouseover", this.onMouseover) + .on("mouseleave", this.onMouseleave); + nodeEnter.append("rect") .attr("width", this.rectW) @@ -219,9 +224,23 @@ class CellsTree extends Object { // update the cell data // probably should be handled by a different class const infoDiv = document.getElementById("cell-info") - infoDiv.textContent = event.name; + infoDiv.textContent = `${event.name} (id: ${event.identity})`; + } + + onMouseover(event){ + // highlighte the corresponding element in the target window + chrome.devtools.inspectedWindow.eval( + `document.querySelector("[data-cell-id='${event.identity}']").classList.add('devtools-inspect')'` + ); } + onMouseleave(event){ + console.log(event); + // highlighte the corresponding element in the target window + chrome.devtools.inspectedWindow.eval( + `document.querySelector("[data-cell-id='${event.identity}']").classList.remove('devtools-inspect')'` + ); + } //Redraw for zoom redraw() { this.svg.attr("transform", From ecc9d5259d05350b90c2de4e69b09ee997e25605 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 17 Oct 2022 14:52:34 +0200 Subject: [PATCH 13/57] updaing mouseover and leave --- object_database/web/devtools/js/tree.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/object_database/web/devtools/js/tree.js b/object_database/web/devtools/js/tree.js index 81ec9db44..7f4aede19 100644 --- a/object_database/web/devtools/js/tree.js +++ b/object_database/web/devtools/js/tree.js @@ -230,7 +230,7 @@ class CellsTree extends Object { onMouseover(event){ // highlighte the corresponding element in the target window chrome.devtools.inspectedWindow.eval( - `document.querySelector("[data-cell-id='${event.identity}']").classList.add('devtools-inspect')'` + `document.querySelector("[data-cell-id='${event.identity}']").style.backgroundColor = 'lightblue'` ); } @@ -238,7 +238,7 @@ class CellsTree extends Object { console.log(event); // highlighte the corresponding element in the target window chrome.devtools.inspectedWindow.eval( - `document.querySelector("[data-cell-id='${event.identity}']").classList.remove('devtools-inspect')'` + `document.querySelector("[data-cell-id='${event.identity}']").style.backgroundColor= 'initial'` ); } //Redraw for zoom From cc0608ceceb3fe4ea83d1fb6bb644b8a163f8252 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 17 Oct 2022 15:34:44 +0200 Subject: [PATCH 14/57] updating devtools readme --- object_database/web/devtools/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/object_database/web/devtools/README.md b/object_database/web/devtools/README.md index 9a9b7a09c..eadb20b2b 100644 --- a/object_database/web/devtools/README.md +++ b/object_database/web/devtools/README.md @@ -1,4 +1,4 @@ -## Browser Devtools Extensions ## +## Chrome Devtools Extensions ## The code here is devoted to building devtool extensions and related development. @@ -10,7 +10,7 @@ Open the extension manager in chrome. Then press "Load Temporary Add-on," select ### Development #### -At its core the devtools extension is configured and defined by a [manifest.json](./manifest.json) file [NOTE: currently this is written for Manifest V2 which will no longer be supported in 2023]. The internals of the configuration is pretty self explanatory but the key thing to note is the presence of a `devtools_page` key. This specifies that the current is a "devtools extension" as opposed to a generic browser extension. You can read more about manifests [here](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json). +At its core the devtools extension is configured and defined by a [manifest.json](./manifest.json) file [NOTE: currently this is written for Manifest V2 which will no longer be supported in 2023]. The internals of the configuration is pretty self explanatory but the key thing to note is the presence of a `devtools_page` key. This specifies that the current is a "devtools extension" as opposed to a generic chrome extension. You can read more about manifests [here](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json). After loading the extension as above you can see your changes by pressing the `reload` button. You can also load a devtools for the devtools by pressing `Inspect`, but the functionalities are limited (for example there is no view on the DOM, since there is no DOM here in the standard sense). @@ -27,7 +27,7 @@ Lets go through these one at a time: With the extension loaded as above, when the devtools open it will load the `devools_page` specified in the manifest file. Even though [devtools.html](./devtools.html) is technically an html page, devtools pages **do not** have any visible DOM or access to WebExtension API's. What they do have is JS source code included via the `script` tag. In our case this is the [devtools_init.js](./devtools_init.js) file. -This bundled source file **does** have access to DOM API's through the global `window` object, same WebExtension API as content scripts (see below), and devtools APIs. The first thing this script does is use the latter to [create and configure the panel](./devtools_init.js#L9). Then subsequently it sets up global variables for holding data, callbacks for `panel.onShow` (fired for example when the panel icon is clicked) and `panel.onHidden`, as well as opening a `browser.runtime.Port` which will allow for communication between the panel and the [background.js](./background.js) processes (see more on that below). +This bundled source file **does** have access to DOM API's through the global `window` object, same WebExtension API as content scripts (see below), and devtools APIs. The first thing this script does is use the latter to [create and configure the panel](./devtools_init.js#L9). Then subsequently it sets up global variables for holding data, callbacks for `panel.onShow` (fired for example when the panel icon is clicked) and `panel.onHidden`, as well as opening a `chrome.runtime.Port` which will allow for communication between the panel and the [background.js](./background.js) processes (see more on that below). Note: since `devtools_init.js` has access to the `window` object, which is the context that panel lives in, we can call functions or access variable between the two. For example, [window.handleMessageFromBackground()](./devtools_init.js#L25) is defined in [cell_panel.js](./js/cell_panel.js) but we can still access it through `_window`. @@ -39,8 +39,8 @@ Note also, the var `_window` is used here to deal with the possibility that at t The files you find in [js](./js) are what you expect of every normal web application: there is html, js and css. As of writing there are two interesting pieces here: -* the `handleMessageFromBackground()` function, already seen above called in `devtools_init.js`, is the callback for a message coming in from [background.js](background.js) via the panel port connection set up in `devtools_init.js`. The function handles the incoming message and updates in the display in the panel accordingly. As of writing it simply cleans up the message parts and displays them as pretty strings in a table row, -* for every object (such as `button`) coming over in the message it adds an `on click` callback. This callback uses the `browser.devtools.inspectedWindow.eval` API to insert a raw script and execute it on the `document`, i.e. on the application side, then listen to and handle the result. This allows for a way to by-pass the "standard" extension communication protocol and directly interact with the target window, although in a somewhat limited way. Is it safe to call `eval()` on raw scripts strings - depends... so use with caution. +* the `handleMessageFromBackground()` function, already seen above called in `devtools_init.js`, is the callback for a message coming in from [background.js](background.js) via the panel port connection set up in `devtools_init.js`. The function handles the incoming message and updates in the display in the panel accordingly. As of writing it cases on `msg.status` (initial load, reconnecting, loaded) and calls for corresponding display functions, +* Note the `mouseover` and `mouseleave` event handlers added to the tree in [tree.js](./js/tree.js). This callback uses the `chrome.devtools.inspectedWindow.eval` API to insert a raw script and execute it on the `document`, i.e. on the inspected window. This allows for a way to by-pass the "standard" extension communication protocol and directly interact with the target window, although in a somewhat limited way. Is it safe to call `eval()` on raw scripts strings - depends... so use with caution. In short, the code in panels is what you see when you click on the devtools message inspector icon and it communicates with the rest of the world either via `backround.js` or via direct script insertions. @@ -51,14 +51,14 @@ In short, the code in panels is what you see when you click on the devtools mess [background](background.js) is the bridge communication between the devtools panel and the target application (see more on this in the content-script description below). It -* listens for `browser.runtime.Port` communications from both the `content-script.js` and the `cell_panel.js`, +* listens for `chrome.runtime.Port` communications from both the `content-script.js` and the `cell_panel.js`, * routes messages as needed via the various open ports. [content-script.js](./content-script.js) ---------------------------------------- -This script is injected into the target document window and runs each time devtools is open, i.e. everything here is essentially the same as any js code that you import via the `script` tag into your application. The key exception is that **content scripts have access to the background script** defined processes and code via the `browser.runtime.Port` API **and** it can communicate with the your application (in our case via the [window.postMessage()](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) API. +This script is injected into the target document window and runs each time devtools is open, i.e. everything here is essentially the same as any js code that you import via the `script` tag into your application. The key exception is that **content scripts have access to the background script** defined processes and code via the `chrome.runtime.Port` API **and** it can communicate with the your application (in our case via the [window.postMessage()](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) API. Since devtools doesn't have direct access to the target window API, `background.js` becomes the bridge for all communication. From 2ec08b7846e01d071586f98ff06ff0b541a692d5 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 17 Oct 2022 15:52:24 +0200 Subject: [PATCH 15/57] adding a better check (on the devtools side) to see if tree updated --- object_database/web/devtools/js/cell_panel.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index fc5196de7..66a16a53b 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -2,7 +2,7 @@ import {CellsTree} from './tree.js'; // GLOBALS (TODO: should be handled better) let state = null; - +let cellsJSONCache = null; // setup message handling from background function handleMessageFromBackground(msg){ @@ -21,8 +21,10 @@ function handleMessageFromBackground(msg){ } break; case "loaded": - if(state != msg.status){ + // check to see if the cells tree has changed + if(cellsJSONCache != JSON.stringify(msg.cells)){ state = msg.status; + cellsJSONCache = JSON.stringify(msg.cells); cellsTreeDisplay(msg.cells); console.log(msg.cells); } From f83015612a37c17c0bbe77e9fddfb173524da1ee Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 4 Nov 2022 14:49:20 +0100 Subject: [PATCH 16/57] limiting the node name length in tree --- object_database/web/devtools/js/tree.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/object_database/web/devtools/js/tree.js b/object_database/web/devtools/js/tree.js index 7f4aede19..a8ac2594f 100644 --- a/object_database/web/devtools/js/tree.js +++ b/object_database/web/devtools/js/tree.js @@ -106,7 +106,13 @@ class CellsTree extends Object { .attr("dy", ".35em") .attr("text-anchor", "middle") .text(function (d) { - return d.name; + // limit the name to 7 chars since text-overflow + // doesn't seem to work here + let name = d.name; + if (name.length > 7){ + name = name.slice(0, 7) + "..."; + } + return name; } ); From 70969b2208dcba29b2aef8145379cef4be5a7326 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 4 Nov 2022 16:39:53 +0100 Subject: [PATCH 17/57] template setup for tree component --- object_database/web/devtools/tree/Tree.js | 35 +++++++++++++++++++ .../web/devtools/tree/examples/tree.html | 18 ++++++++++ .../web/devtools/tree/package.json | 14 ++++++++ .../web/devtools/tree/tests/component.js | 19 ++++++++++ .../web/devtools/tree/tests/test-setup.js | 18 ++++++++++ 5 files changed, 104 insertions(+) create mode 100644 object_database/web/devtools/tree/Tree.js create mode 100644 object_database/web/devtools/tree/examples/tree.html create mode 100644 object_database/web/devtools/tree/package.json create mode 100644 object_database/web/devtools/tree/tests/component.js create mode 100644 object_database/web/devtools/tree/tests/test-setup.js diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js new file mode 100644 index 000000000..3d3efae58 --- /dev/null +++ b/object_database/web/devtools/tree/Tree.js @@ -0,0 +1,35 @@ +/** + * Tree Graph Web component + **/ + + +// Simple grid-based sheet component +const templateString = ` + +
A Tree
+`; + +class Tree extends HTMLElement { + constructor() { + super(); + this.template = document.createElement("template"); + this.template.innerHTML = templateString; + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(this.template.content.cloneNode(true)); + } + + get ok(){ + return "ok"; + } +} + +window.customElements.define("my-tree", Tree); + +export { Tree as default, Tree } diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html new file mode 100644 index 000000000..0a098e1aa --- /dev/null +++ b/object_database/web/devtools/tree/examples/tree.html @@ -0,0 +1,18 @@ + + + + Basic Grid Tests + + + + + + + + + diff --git a/object_database/web/devtools/tree/package.json b/object_database/web/devtools/tree/package.json new file mode 100644 index 000000000..fc27b4de3 --- /dev/null +++ b/object_database/web/devtools/tree/package.json @@ -0,0 +1,14 @@ +{ + "type": "module", + "devDependencies": { + "chai": "^4.3.4", + "esm": "^3.2.25", + "jsdom": "^19.0.0", + "jsdom-global": "^3.0.2", + "mocha": "^10.0.0", + "prettier": "^2.7.1" + }, + "scripts": { + "test": "mocha --require ./tests/test-setup.js ./tests/" + } +} diff --git a/object_database/web/devtools/tree/tests/component.js b/object_database/web/devtools/tree/tests/component.js new file mode 100644 index 000000000..a942e54bf --- /dev/null +++ b/object_database/web/devtools/tree/tests/component.js @@ -0,0 +1,19 @@ +/** + * Core tests for the Tree component + **/ + +import chai from "chai"; +const assert = chai.assert; + +import { Tree } from '../tree.js'; + +describe("Tree Component Tests", () => { + const tree = document.createElement("my-tree"); + beforeEach(() => { + }); + it("Ok", () => { + assert.equal(tree.ok, "ok"); + }); +}) + + diff --git a/object_database/web/devtools/tree/tests/test-setup.js b/object_database/web/devtools/tree/tests/test-setup.js new file mode 100644 index 000000000..01ad11442 --- /dev/null +++ b/object_database/web/devtools/tree/tests/test-setup.js @@ -0,0 +1,18 @@ +// Preload file for JSDOM required tests +import { JSDOM } from "jsdom"; + +const resetDOM = () => { + const dom = new JSDOM( + "/>/>" + ); + globalThis.window = dom.window; + globalThis.document = dom.window.document; + globalThis.HTMLElement = dom.window.HTMLElement; + globalThis.customElements = dom.window.customElements; + globalThis.CustomEvent = dom.window.CustomEvent; + globalThis.KeyboardEvent = dom.window.KeyboardEvent; +}; + +globalThis.resetDOM = resetDOM; + +resetDOM(); From 8f8fcf36dd0824af1e0f3224f2cc29b7d694e91b Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 4 Nov 2022 21:37:14 +0100 Subject: [PATCH 18/57] adding TreeNode and updating basic example setup --- object_database/web/devtools/tree/Tree.js | 34 +++++++++++++++++-- object_database/web/devtools/tree/TreeNode.js | 33 ++++++++++++++++++ .../web/devtools/tree/examples/tree.html | 33 +++++++++++++++--- .../web/devtools/tree/package.json | 3 ++ 4 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 object_database/web/devtools/tree/TreeNode.js diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 3d3efae58..9f084401a 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -13,7 +13,7 @@ const templateString = ` } -
A Tree
+
A Tree
`; class Tree extends HTMLElement { @@ -23,13 +23,43 @@ class Tree extends HTMLElement { this.template.innerHTML = templateString; this.attachShadow({ mode: "open" }); this.shadowRoot.appendChild(this.template.content.cloneNode(true)); + + // bind methods + this.setup = this.setup.bind(this); + this.setupNode = this.setupNode.bind(this); + } + + connectedCallback(){ + if(this.isConnected){ + this.setup(); + } } + setup(data){ + this.setupNode(data); + } + + + setupNode(nodeData){ + if(nodeData){ + const wrapper = this.shadowRoot.querySelector("#wrapper"); + const node = document.createElement("tree-node"); + wrapper.append(node); + // setup the children + nodeData.children.forEach((childData) => { + this.setupNode(childData); + }) + } + } + + + + get ok(){ return "ok"; } } -window.customElements.define("my-tree", Tree); +window.customElements.define("tree-graph", Tree); export { Tree as default, Tree } diff --git a/object_database/web/devtools/tree/TreeNode.js b/object_database/web/devtools/tree/TreeNode.js new file mode 100644 index 000000000..accc10d4a --- /dev/null +++ b/object_database/web/devtools/tree/TreeNode.js @@ -0,0 +1,33 @@ +/** + * Tree Node Web component + **/ + + +// Simple grid-based sheet component +const templateString = ` + +
A Node
+`; + +class TreeNode extends HTMLElement { + constructor() { + super(); + this.template = document.createElement("template"); + this.template.innerHTML = templateString; + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(this.template.content.cloneNode(true)); + } + + get ok(){ + return "ok"; + } +} + +window.customElements.define("tree-node", TreeNode); + +export { TreeNode as default, TreeNode } diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index 0a098e1aa..f9c50bbe0 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -4,15 +4,38 @@ Basic Grid Tests + - - +
diff --git a/object_database/web/devtools/tree/package.json b/object_database/web/devtools/tree/package.json index fc27b4de3..b33e0cbc2 100644 --- a/object_database/web/devtools/tree/package.json +++ b/object_database/web/devtools/tree/package.json @@ -10,5 +10,8 @@ }, "scripts": { "test": "mocha --require ./tests/test-setup.js ./tests/" + }, + "dependencies": { + "leader-line": "^1.0.7" } } From 7fbf00cdd501908e5d3c85c066d456696ebbe6dd Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sat, 5 Nov 2022 22:21:33 +0100 Subject: [PATCH 19/57] adding depths to trees and webpack --- object_database/web/devtools/tree/Tree.js | 36 +++++++++++++------ object_database/web/devtools/tree/TreeNode.js | 10 ++++-- .../web/devtools/tree/examples/tree.html | 3 +- object_database/web/devtools/tree/main.js | 6 ++++ .../web/devtools/tree/package.json | 8 +++-- .../web/devtools/tree/webpack.config.js | 30 ++++++++++++++++ 6 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 object_database/web/devtools/tree/main.js create mode 100644 object_database/web/devtools/tree/webpack.config.js diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 9f084401a..5ae8fd78c 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -2,6 +2,7 @@ * Tree Graph Web component **/ +import LeaderLine from "leader-line"; // Simple grid-based sheet component const templateString = ` @@ -12,8 +13,13 @@ const templateString = ` overflow: hidden; /* For auto-resize without scrolling on */ } +.depth { + display: flex; + justify-content: space-around; +} + -
A Tree
+
`; class Tree extends HTMLElement { @@ -36,24 +42,34 @@ class Tree extends HTMLElement { } setup(data){ - this.setupNode(data); + const wrapper = this.shadowRoot.querySelector("#wrapper"); + const nodeDepth = document.createElement("div"); + nodeDepth.classList.add("depth"); + wrapper.append(nodeDepth); + this.setupNode(data, nodeDepth); } - - setupNode(nodeData){ + setupNode(nodeData, nodeDepth){ if(nodeData){ - const wrapper = this.shadowRoot.querySelector("#wrapper"); const node = document.createElement("tree-node"); - wrapper.append(node); - // setup the children + node.name = nodeData.name; + node.setAttribute("id", nodeData.id); + nodeDepth.append(node); + // setup the children in a new node depth + const wrapper = this.shadowRoot.querySelector("#wrapper"); + const childrenDepth = document.createElement("div"); + childrenDepth.classList.add("depth"); + wrapper.append(childrenDepth); nodeData.children.forEach((childData) => { - this.setupNode(childData); + this.setupNode(childData, childrenDepth); + }) } } - - + updateLeaderLines(depth){ + //TODO: + } get ok(){ return "ok"; diff --git a/object_database/web/devtools/tree/TreeNode.js b/object_database/web/devtools/tree/TreeNode.js index accc10d4a..af509386f 100644 --- a/object_database/web/devtools/tree/TreeNode.js +++ b/object_database/web/devtools/tree/TreeNode.js @@ -11,7 +11,9 @@ const templateString = ` } -
A Node
+
+ +
`; class TreeNode extends HTMLElement { @@ -23,8 +25,10 @@ class TreeNode extends HTMLElement { this.shadowRoot.appendChild(this.template.content.cloneNode(true)); } - get ok(){ - return "ok"; + set name(s){ + const name = this.shadowRoot.querySelector("#name"); + name.innerText = s; + } } diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index f9c50bbe0..082674f9e 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -3,8 +3,7 @@ Basic Grid Tests - - +
diff --git a/object_database/web/devtools/tree/main.js b/object_database/web/devtools/tree/main.js new file mode 100644 index 000000000..9109ce112 --- /dev/null +++ b/object_database/web/devtools/tree/main.js @@ -0,0 +1,6 @@ +/* Main */ +import { Tree } from "./Tree.js"; +import { TreeNode } from "./TreeNode.js"; +import LeaderLine from "leader-line"; + +export { Tree, TreeNode, LeaderLine, Tree as default }; diff --git a/object_database/web/devtools/tree/package.json b/object_database/web/devtools/tree/package.json index b33e0cbc2..8361a9956 100644 --- a/object_database/web/devtools/tree/package.json +++ b/object_database/web/devtools/tree/package.json @@ -9,9 +9,13 @@ "prettier": "^2.7.1" }, "scripts": { - "test": "mocha --require ./tests/test-setup.js ./tests/" + "test": "mocha --require ./tests/test-setup.js ./tests/", + "build": "./node_modules/webpack/bin/webpack.js" }, "dependencies": { - "leader-line": "^1.0.7" + "leader-line": "^1.0.7", + "skeleton-loader": "^2.0.0", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0" } } diff --git a/object_database/web/devtools/tree/webpack.config.js b/object_database/web/devtools/tree/webpack.config.js new file mode 100644 index 000000000..4961f974c --- /dev/null +++ b/object_database/web/devtools/tree/webpack.config.js @@ -0,0 +1,30 @@ +import path from "path"; +import * as url from "url"; +const __filename = url.fileURLToPath(import.meta.url); +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); + +let config = { + entry: "./main.js", + output: { + path: path.resolve(__dirname, "dist/"), + filename: "tree.bundle.js", + }, + module: { + rules: [ + { + test: path.resolve(__dirname, "node_modules/leader-line/"), + use: [ + { + loader: "skeleton-loader", + options: { + procedure: (content) => `${content}export default LeaderLine`, + }, + }, + ], + }, + ], + }, + mode: "development", +}; + +export { config as default }; From 947d46fb1008a631e1f3ab28cec549b0b5288423 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 7 Nov 2022 21:12:42 +0100 Subject: [PATCH 20/57] [WIP] rewriting Tree as regular object to dealing with leaderlines --- object_database/web/devtools/tree/Tree.js | 61 ++++++++++++++----- .../web/devtools/tree/examples/tree.html | 56 ++++++++++------- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 5ae8fd78c..80b0fbbed 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -16,6 +16,13 @@ const templateString = ` .depth { display: flex; justify-content: space-around; + border: 2px black solid; +} + +.child-wrapper { + display: flex; + justify-content: space-around; + border: 1px blue solid; } @@ -33,42 +40,68 @@ class Tree extends HTMLElement { // bind methods this.setup = this.setup.bind(this); this.setupNode = this.setupNode.bind(this); + this.setupLeaderLines = this.setupLeaderLines.bind(this); } connectedCallback(){ if(this.isConnected){ - this.setup(); + // this.setup(); } } setup(data){ - const wrapper = this.shadowRoot.querySelector("#wrapper"); + // const wrapper = this.shadowRoot.querySelector("#wrapper"); + const wrapper = document.body; const nodeDepth = document.createElement("div"); nodeDepth.classList.add("depth"); + nodeDepth.setAttribute("id", "depth-0"); wrapper.append(nodeDepth); - this.setupNode(data, nodeDepth); + const nodeWrapper = document.createElement("div"); + nodeWrapper.classList.add("child-wrapper"); + nodeDepth.append(nodeWrapper); + this.setupNode(data, nodeWrapper, 1); } - setupNode(nodeData, nodeDepth){ + setupNode(nodeData, wrapperDiv, depth){ if(nodeData){ + console.log(depth); const node = document.createElement("tree-node"); node.name = nodeData.name; node.setAttribute("id", nodeData.id); - nodeDepth.append(node); + wrapperDiv.append(node); // setup the children in a new node depth - const wrapper = this.shadowRoot.querySelector("#wrapper"); - const childrenDepth = document.createElement("div"); - childrenDepth.classList.add("depth"); - wrapper.append(childrenDepth); - nodeData.children.forEach((childData) => { - this.setupNode(childData, childrenDepth); + if(nodeData.children.length){ + // if the corresponding depth has not been added, do so now + let depthDiv = document.body.querySelector(`#depth-${depth}`); + if (!depthDiv) { + depthDiv = document.createElement("div"); + depthDiv.setAttribute("id", `depth-${depth}`); + depthDiv.classList.add("depth"); + // const wrapper = this.shadowRoot.querySelector("#wrapper"); + const wrapper = document.body; + wrapper.append(depthDiv); + } + const childWrapper = document.createElement("div"); + childWrapper.classList.add("child-wrapper"); + depthDiv.append(childWrapper); + nodeData.children.forEach((childData) => { + const child = this.setupNode(childData, childWrapper, depth + 1); + // this.updateLeaderLines(node, child); - }) + }) + } + return node; } } - updateLeaderLines(depth){ - //TODO: + setupLeaderLines(data){ + if (data && data.children) { + const parent = document.body.querySelector(`tree-node#${data.id}`); + data.children.forEach((cdata) => { + const child = document.body.querySelector(`tree-node#${cdata.id}`); + new LeaderLine(parent, child); + }) + } } get ok(){ diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index 082674f9e..5ff6e59cb 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -1,7 +1,7 @@ - Basic Grid Tests + Basic Tree Tests @@ -9,32 +9,42 @@
From 44676d4901f6000ed641789ec24f101cfda511b5 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Tue, 8 Nov 2022 13:34:48 +0100 Subject: [PATCH 21/57] [WIP] add tree.css and minor updates to leaderlines --- object_database/web/devtools/tree/Tree.js | 2 +- object_database/web/devtools/tree/examples/tree.html | 4 ++-- object_database/web/devtools/tree/tree.css | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 object_database/web/devtools/tree/tree.css diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 80b0fbbed..ae3fc1326 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -86,7 +86,7 @@ class Tree extends HTMLElement { depthDiv.append(childWrapper); nodeData.children.forEach((childData) => { const child = this.setupNode(childData, childWrapper, depth + 1); - // this.updateLeaderLines(node, child); + const l = new LeaderLine(node, child); }) } diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index 5ff6e59cb..831bd0a1c 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -2,7 +2,7 @@ Basic Tree Tests - + @@ -44,7 +44,7 @@ const main = document.getElementById("main"); main.append(tree); tree.setup(data); - tree.setupLeaderLines(data); + // tree.setupLeaderLines(data); }); diff --git a/object_database/web/devtools/tree/tree.css b/object_database/web/devtools/tree/tree.css new file mode 100644 index 000000000..065fc9a45 --- /dev/null +++ b/object_database/web/devtools/tree/tree.css @@ -0,0 +1,11 @@ +.depth { + display: flex; + justify-content: space-around; + border: 2px black solid; +} + +.child-wrapper { + display: flex; + justify-content: space-around; + border: 1px blue solid; +} From de7677bd1cbdb16f5af8bb8d6031035afb06312a Mon Sep 17 00:00:00 2001 From: dkrasner Date: Tue, 15 Nov 2022 14:09:52 +0100 Subject: [PATCH 22/57] v0 of working tree with conneiton lines --- object_database/web/devtools/tree/Tree.js | 81 ++++++++++++++----- object_database/web/devtools/tree/TreeNode.js | 5 +- .../web/devtools/tree/examples/tree.html | 6 +- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index ae3fc1326..d1c3d5d88 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -2,7 +2,6 @@ * Tree Graph Web component **/ -import LeaderLine from "leader-line"; // Simple grid-based sheet component const templateString = ` @@ -16,13 +15,15 @@ const templateString = ` .depth { display: flex; justify-content: space-around; - border: 2px black solid; + margin-top: 20px; + margin-bottom: 20px; } .child-wrapper { + margin-top: 5px; + margin-bottom: 5px; display: flex; justify-content: space-around; - border: 1px blue solid; } @@ -40,18 +41,19 @@ class Tree extends HTMLElement { // bind methods this.setup = this.setup.bind(this); this.setupNode = this.setupNode.bind(this); - this.setupLeaderLines = this.setupLeaderLines.bind(this); + this.setupPaths = this.setupPaths.bind(this); + this.addSVGPath = this.addSVGPath.bind(this); } connectedCallback(){ if(this.isConnected){ // this.setup(); + } } setup(data){ - // const wrapper = this.shadowRoot.querySelector("#wrapper"); - const wrapper = document.body; + const wrapper = this.shadowRoot.querySelector("#wrapper"); const nodeDepth = document.createElement("div"); nodeDepth.classList.add("depth"); nodeDepth.setAttribute("id", "depth-0"); @@ -60,11 +62,11 @@ class Tree extends HTMLElement { nodeWrapper.classList.add("child-wrapper"); nodeDepth.append(nodeWrapper); this.setupNode(data, nodeWrapper, 1); + this.setupPaths(data); } setupNode(nodeData, wrapperDiv, depth){ if(nodeData){ - console.log(depth); const node = document.createElement("tree-node"); node.name = nodeData.name; node.setAttribute("id", nodeData.id); @@ -72,21 +74,20 @@ class Tree extends HTMLElement { // setup the children in a new node depth if(nodeData.children.length){ // if the corresponding depth has not been added, do so now - let depthDiv = document.body.querySelector(`#depth-${depth}`); + let depthDiv = this.shadowRoot.querySelector(`#depth-${depth}`); if (!depthDiv) { depthDiv = document.createElement("div"); depthDiv.setAttribute("id", `depth-${depth}`); depthDiv.classList.add("depth"); - // const wrapper = this.shadowRoot.querySelector("#wrapper"); - const wrapper = document.body; + const wrapper = this.shadowRoot.querySelector("#wrapper"); wrapper.append(depthDiv); } const childWrapper = document.createElement("div"); childWrapper.classList.add("child-wrapper"); depthDiv.append(childWrapper); nodeData.children.forEach((childData) => { - const child = this.setupNode(childData, childWrapper, depth + 1); - const l = new LeaderLine(node, child); + this.setupNode(childData, childWrapper, depth + 1); + // this.addSVGPath(node, child); }) } @@ -94,15 +95,59 @@ class Tree extends HTMLElement { } } - setupLeaderLines(data){ - if (data && data.children) { - const parent = document.body.querySelector(`tree-node#${data.id}`); - data.children.forEach((cdata) => { - const child = document.body.querySelector(`tree-node#${cdata.id}`); - new LeaderLine(parent, child); + setupPaths(nodeData){ + if (nodeData) { + const parent = this.shadowRoot.querySelector(`#${nodeData.id}`); + nodeData.children.forEach((childData) => { + const child = this.shadowRoot.querySelector(`#${childData.id}`); + this.addSVGPath(parent, child); + this.setupPaths(childData); }) } } + /** + * I add an SVG bezier curve which starts at the bottom middle + * of the startNode and ends at the top middle of the endNode + * @param {element} startNode + * @param {element} endNode + */ + addSVGPath(startNode, endNode){ + const wrapper = this.shadowRoot.querySelector("#wrapper"); + const startRect = startNode.getBoundingClientRect(); + + const startY = startRect.bottom; + const startX = startRect.left + (startRect.width / 2); + const endRect = endNode.getBoundingClientRect(); + const endY = endRect.top; + const endX = endRect.left + (endRect.width / 2); + + const temp = document.createElement("div"); + temp.innerHTML = ``; + wrapper.append(temp.childNodes[0]) + /* + const svg = document.createElement("svg"); + svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + svg.setAttribute("width", "100"); + svg.setAttribute("height", "100"); + svg.style.left = `${startX}px`; + svg.style.top = `${startY}px`; + svg.style.position = "absolute"; + + const path = document.createElement("path"); + path.setAttribute("d", `M ${startX} ${startY} C 20 20, 40 20, ${endX} ${endY}`); + path.setAttribute("stroke", "black"); + path.setAttribute("fill", "transparent"); + //svg.append(path); + + const line = document.createElement("line"); + line.setAttribute("x1", "100"); + line.setAttribute("y1", "0"); + line.setAttribute("x2", "0"); + line.setAttribute("y2", "100"); + svg.append(line); + wrapper.append(svg); + */ + } get ok(){ return "ok"; diff --git a/object_database/web/devtools/tree/TreeNode.js b/object_database/web/devtools/tree/TreeNode.js index af509386f..76e7973d9 100644 --- a/object_database/web/devtools/tree/TreeNode.js +++ b/object_database/web/devtools/tree/TreeNode.js @@ -7,7 +7,10 @@ const templateString = ` diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index 831bd0a1c..d013207b5 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -12,7 +12,8 @@ const treeMaker = function(depth, maxChildNum){ let rootNode = { name: "root", - id: "roodID", + id: "roodID", + order: -1, children: [] }; rootNode = addChildren(rootNode, 0, depth, maxChildNum); @@ -25,7 +26,8 @@ for (let i = 1; i <= numChildren; i++){ const child = { name: `node_${depth}_${i}`, - id: `node_${depth}_${i}`, + id: "id-" + crypto.randomUUID(), + order: i, children: [] }; node.children.push( From a94d0cff19367f81a280b0d354582e8f7c3916a4 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Tue, 15 Nov 2022 20:23:33 +0100 Subject: [PATCH 23/57] leadrlines use proper js document.createElementNS API adding tests for the tree component --- object_database/web/devtools/tree/Tree.js | 44 ++++++----- .../web/devtools/tree/tests/component.js | 74 +++++++++++++++++-- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index d1c3d5d88..9d6dc5c73 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -108,6 +108,8 @@ class Tree extends HTMLElement { /** * I add an SVG bezier curve which starts at the bottom middle * of the startNode and ends at the top middle of the endNode + * NODE: svg-type elemnents need to be created using the + * SVG name space, ie document.createElementNS... * @param {element} startNode * @param {element} endNode */ @@ -121,31 +123,33 @@ class Tree extends HTMLElement { const endY = endRect.top; const endX = endRect.left + (endRect.width / 2); - const temp = document.createElement("div"); - temp.innerHTML = ``; - wrapper.append(temp.childNodes[0]) + const svg = document.createElementNS('http://www.w3.org/2000/svg', "svg"); + svg.setAttribute("width", "100%"); + svg.setAttribute("height", "100%"); + svg.setAttribute("data-start-node-id", startNode.id); + svg.setAttribute("data-end-node-id", endNode.id); + svg.style.left = 0; + svg.style.top = 0; + + const line = document.createElementNS('http://www.w3.org/2000/svg', "line"); + line.setAttribute("x1", `${startX}`); + line.setAttribute("y1", `${startY}`); + line.setAttribute("x2", `${endX}`); + line.setAttribute("y2", `${endY}`); + line.setAttribute("stroke", "black"); + svg.append(line); + wrapper.append(svg); /* - const svg = document.createElement("svg"); - svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); - svg.setAttribute("width", "100"); - svg.setAttribute("height", "100"); - svg.style.left = `${startX}px`; - svg.style.top = `${startY}px`; - svg.style.position = "absolute"; - - const path = document.createElement("path"); - path.setAttribute("d", `M ${startX} ${startY} C 20 20, 40 20, ${endX} ${endY}`); + const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); + path.setAttribute("d", `M ${startX} ${startY} Q 95 10, ${endX} ${endY}`); path.setAttribute("stroke", "black"); path.setAttribute("fill", "transparent"); - //svg.append(path); + svg.append(path); - const line = document.createElement("line"); - line.setAttribute("x1", "100"); - line.setAttribute("y1", "0"); - line.setAttribute("x2", "0"); - line.setAttribute("y2", "100"); - svg.append(line); wrapper.append(svg); + const temp = document.createElement("div"); + temp.innerHTML = ``; + wrapper.append(temp.childNodes[0]) */ } diff --git a/object_database/web/devtools/tree/tests/component.js b/object_database/web/devtools/tree/tests/component.js index a942e54bf..85f344318 100644 --- a/object_database/web/devtools/tree/tests/component.js +++ b/object_database/web/devtools/tree/tests/component.js @@ -3,16 +3,80 @@ **/ import chai from "chai"; +import crypto from 'crypto'; +import { Tree } from '../tree.js'; const assert = chai.assert; -import { Tree } from '../tree.js'; +const treeMaker = function(depth, maxChildNum){ + let rootNode = { + name: "root", + id: "roodID", + order: -1, + children: [] + }; + rootNode = addChildren(rootNode, 0, depth, maxChildNum); + return rootNode; +} + +const addChildren = (node, depth, totalDepth, maxChildNum) => { + const numChildren = Math.ceil(Math.random()* maxChildNum); + if (depth <= totalDepth) { + for (let i = 1; i <= numChildren; i++){ + const child = { + name: `node_${depth}_${i}`, + id: "id-" + crypto.randomUUID(), + order: i, + children: [] + }; + node.children.push( + addChildren(child, depth + 1, totalDepth, maxChildNum) + ); + + } + } + return node; +} + describe("Tree Component Tests", () => { - const tree = document.createElement("my-tree"); - beforeEach(() => { + let tree; + let data + before(() => { + tree = document.createElement("tree-graph"); + data = treeMaker(3, 2); + tree.setup(data); + document.body.append(tree); + }); + it("Assert the tree exists", () => { + assert.exists(tree); + }); + it("Tree has a node for each element in the data", () => { + const nodeCheck = (nodeData) => { + if (nodeData) { + const node = tree.shadowRoot.querySelector(`#${nodeData.id}`); + assert.exists(node); + nodeData.children.forEach((childData) => { + const childNode = tree.shadowRoot.querySelector(`#${childData.id}`); + assert.exists(childNode); + nodeCheck(childData); + }) + } + } + nodeCheck(data); }); - it("Ok", () => { - assert.equal(tree.ok, "ok"); + it("Tree has leaderlines for every parent-child node", () => { + const leaderlineCheck = (nodeData) => { + if (nodeData) { + nodeData.children.forEach((childData) => { + const svg = tree.shadowRoot.querySelector( + `svg[data-start-node-id="${nodeData.id}"][data-end-node-id="${childData.id}"]` + ); + assert.exists(svg); + leaderlineCheck(childData); + }) + } + } + leaderlineCheck(data); }); }) From 64175ad0d5e73f61478b0a125c58954f9eb9020a Mon Sep 17 00:00:00 2001 From: dkrasner Date: Wed, 16 Nov 2022 13:33:09 +0100 Subject: [PATCH 24/57] adding global css, better styling and window resize listener --- object_database/web/devtools/tree/Tree.js | 43 +++++++++++-------- object_database/web/devtools/tree/TreeNode.js | 3 ++ object_database/web/devtools/tree/tree.css | 18 ++++---- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 9d6dc5c73..aa9bd1009 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -15,8 +15,8 @@ const templateString = ` .depth { display: flex; justify-content: space-around; - margin-top: 20px; - margin-bottom: 20px; + margin-top: 30px; + margin-bottom: 30px; } .child-wrapper { @@ -38,21 +38,27 @@ class Tree extends HTMLElement { this.attachShadow({ mode: "open" }); this.shadowRoot.appendChild(this.template.content.cloneNode(true)); + this.data; + // bind methods this.setup = this.setup.bind(this); + this.clear = this.clear.bind(this); this.setupNode = this.setupNode.bind(this); this.setupPaths = this.setupPaths.bind(this); this.addSVGPath = this.addSVGPath.bind(this); + this.onWindowResize = this.onWindowResize.bind(this); } connectedCallback(){ if(this.isConnected){ - // this.setup(); - + // add event listeners + window.addEventListener("resize", this.onWindowResize); } } setup(data){ + // cache the data; TODO: think through this + this.data = data; const wrapper = this.shadowRoot.querySelector("#wrapper"); const nodeDepth = document.createElement("div"); nodeDepth.classList.add("depth"); @@ -128,7 +134,8 @@ class Tree extends HTMLElement { svg.setAttribute("height", "100%"); svg.setAttribute("data-start-node-id", startNode.id); svg.setAttribute("data-end-node-id", endNode.id); - svg.style.left = 0; + svg.style.position = "absolute"; + svg.style.left = 0; svg.style.top = 0; const line = document.createElementNS('http://www.w3.org/2000/svg', "line"); @@ -136,25 +143,23 @@ class Tree extends HTMLElement { line.setAttribute("y1", `${startY}`); line.setAttribute("x2", `${endX}`); line.setAttribute("y2", `${endY}`); - line.setAttribute("stroke", "black"); + line.setAttribute("stroke", "var(--palette-blue)"); + line.setAttribute("stroke-width", "3px"); svg.append(line); wrapper.append(svg); - /* - const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); - path.setAttribute("d", `M ${startX} ${startY} Q 95 10, ${endX} ${endY}`); - path.setAttribute("stroke", "black"); - path.setAttribute("fill", "transparent"); - svg.append(path); + } - wrapper.append(svg); - const temp = document.createElement("div"); - temp.innerHTML = ``; - wrapper.append(temp.childNodes[0]) - */ + /** + * On a window resize I first clear the tree and then redraw it + **/ + onWindowResize(event){ + this.clear(); + this.setup(this.data); } - get ok(){ - return "ok"; + clear(){ + const wrapper = this.shadowRoot.querySelector("#wrapper"); + wrapper.innerHTML = ""; } } diff --git a/object_database/web/devtools/tree/TreeNode.js b/object_database/web/devtools/tree/TreeNode.js index 76e7973d9..db4073d4a 100644 --- a/object_database/web/devtools/tree/TreeNode.js +++ b/object_database/web/devtools/tree/TreeNode.js @@ -11,6 +11,9 @@ const templateString = ` border: 1px blue solid; margin-right: 10px; margin-left: 10px; + background-color: var(--palette-orange); + padding: 5px; + border-radius: 10px; } diff --git a/object_database/web/devtools/tree/tree.css b/object_database/web/devtools/tree/tree.css index 065fc9a45..30489caca 100644 --- a/object_database/web/devtools/tree/tree.css +++ b/object_database/web/devtools/tree/tree.css @@ -1,11 +1,9 @@ -.depth { - display: flex; - justify-content: space-around; - border: 2px black solid; -} - -.child-wrapper { - display: flex; - justify-content: space-around; - border: 1px blue solid; +:root { + --palette-beige: #FBE8A6; + --palette-blue: #303C6C; + --palette-lightblue: #B4DFE5; + --palette-cyan: #D2FDFF; + --palette-orange: #F4976C; + --palette-black: black; + --palette-white: white; } From 6f75f80c592015cfad49489f9687d0c88e44e4e4 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Wed, 30 Nov 2022 13:10:26 +0100 Subject: [PATCH 25/57] adding subtree zoom in/out and related css updates to tree and node adding a readme --- .gitignore | 1 + object_database/web/devtools/tree/README.md | 7 +++ object_database/web/devtools/tree/Tree.js | 54 +++++++++++++++++-- object_database/web/devtools/tree/TreeNode.js | 5 ++ 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 object_database/web/devtools/tree/README.md diff --git a/.gitignore b/.gitignore index d8092b3c2..d8dd43d83 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ object_database/web/content/dist \#* \.#* *~ +.projectile # leading slash means only match at repo-root level /.python-version diff --git a/object_database/web/devtools/tree/README.md b/object_database/web/devtools/tree/README.md new file mode 100644 index 000000000..3dc5e0270 --- /dev/null +++ b/object_database/web/devtools/tree/README.md @@ -0,0 +1,7 @@ +## About ## + +The tree plotting library is a simple way of plotting nested list n-arry tree representations. It is build on two main web-components: the [Tree](./Tree.js) and [TreeNode](./TreeNode.js). The nodes are connected by svg lines and any node of your choosing can be used. IE `Tree` class-element simply expects that there is a `tree-node` element defined in the DOM and that is has some width and height. + +### Installation and Build ### + +In a `nodeenv` run `npm install && npm run build`. To see an example serve the [examples](./examples) directory using `python -m http.server` (or whatever web-server you prefer). diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index aa9bd1009..b8bcf27f0 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -26,6 +26,10 @@ const templateString = ` justify-content: space-around; } +svg { + z-index: -1; +} +
`; @@ -47,6 +51,8 @@ class Tree extends HTMLElement { this.setupPaths = this.setupPaths.bind(this); this.addSVGPath = this.addSVGPath.bind(this); this.onWindowResize = this.onWindowResize.bind(this); + // event handlers + this.onNodeDblclick = this.onNodeDblclick.bind(this); } connectedCallback(){ @@ -56,9 +62,13 @@ class Tree extends HTMLElement { } } - setup(data){ - // cache the data; TODO: think through this - this.data = data; + setup(data, cache=true){ + if (cache){ + // cache the data; TODO: think through this + this.data = data; + } + // mark the first node as the root + data.root = true; const wrapper = this.shadowRoot.querySelector("#wrapper"); const nodeDepth = document.createElement("div"); nodeDepth.classList.add("depth"); @@ -76,9 +86,13 @@ class Tree extends HTMLElement { const node = document.createElement("tree-node"); node.name = nodeData.name; node.setAttribute("id", nodeData.id); + if (nodeData.root) { + node.setAttribute("data-root-node", true); + } wrapperDiv.append(node); + node.addEventListener("dblclick", this.onNodeDblclick); // setup the children in a new node depth - if(nodeData.children.length){ + if (nodeData.children.length) { // if the corresponding depth has not been added, do so now let depthDiv = this.shadowRoot.querySelector(`#depth-${depth}`); if (!depthDiv) { @@ -157,9 +171,39 @@ class Tree extends HTMLElement { this.setup(this.data); } + onNodeDblclick(event){ + this.clear(); + // if clicking on the root node, reset back to cached this.data tree + if (event.target.hasAttribute("data-root-node")) { + this.setup(this.data); + } else { + const id = event.target.id; + const subTree = this.findSubTree(id, this.data); + this.setup(subTree, false); // do not cache this data + } + } + + /** + * I recursively walk the tree to find the corresponding + * node, and when I do I return its subtree + **/ + findSubTree(id, node){ + if(node.id == id) { + return node; + } + let subTree; + node.children.forEach((childNode) => { + const out = this.findSubTree(id, childNode); + if (out) { + subTree = out; + } + }) + return subTree; + } + clear(){ const wrapper = this.shadowRoot.querySelector("#wrapper"); - wrapper.innerHTML = ""; + wrapper.replaceChildren(); } } diff --git a/object_database/web/devtools/tree/TreeNode.js b/object_database/web/devtools/tree/TreeNode.js index db4073d4a..97182fcd3 100644 --- a/object_database/web/devtools/tree/TreeNode.js +++ b/object_database/web/devtools/tree/TreeNode.js @@ -16,6 +16,11 @@ const templateString = ` border-radius: 10px; } +div#wrapper { + max-width: 100px; + text-overflow: ellipsis; + overflow: hidden; +}
From 1344b956458309cef7983a3183a880fb7d6d3bf7 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Wed, 30 Nov 2022 15:42:46 +0100 Subject: [PATCH 26/57] optimizing svg line rendering; adding findParentSubTree for nav --- object_database/web/devtools/tree/Tree.js | 106 +++++++++++++++++----- 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index b8bcf27f0..b7b5df3fa 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -6,12 +6,46 @@ // Simple grid-based sheet component const templateString = `
`; @@ -77,9 +81,6 @@ class Tree extends HTMLElement { this.data; - this.maxDepth = 3; //TODO!!!! - this.attainedDepth; - // bind methods this.setup = this.setup.bind(this); this.clear = this.clear.bind(this); @@ -97,6 +98,7 @@ class Tree extends HTMLElement { // add event listeners window.addEventListener("resize", this.onWindowResize); document.addEventListener("keyup", this.onKeyUp); + this.setAttribute("display-depth", 3); } } @@ -110,8 +112,6 @@ class Tree extends HTMLElement { // cache the data; TODO: think through this this.data = data; } - // TODO depth should be an attribute - this.attainedDepth = 0; const wrapper = this.shadowRoot.querySelector("#wrapper"); wrapper.addEventListener("dblclick", this.onNodeDblclick); const nodeDepth = document.createElement("div"); @@ -130,9 +130,6 @@ class Tree extends HTMLElement { svg.style.left = 0; svg.style.top = 0; wrapper.append(svg); - - // TODO depth should be an attribute - this.attainedDepth = 0; this.setupPaths(svg, data); // fade the wrapper wrapper.classList.remove("animation-fade-out"); @@ -140,8 +137,7 @@ class Tree extends HTMLElement { } setupNode(nodeData, wrapperDiv, depth, root=false){ - if(nodeData && this.attainedDepth <= this.maxDepth){ - this.attainedDepth += 1; // TODO + if(nodeData){ const node = document.createElement("tree-node"); node.name = nodeData.name; node.setAttribute("id", nodeData.id); @@ -152,6 +148,12 @@ class Tree extends HTMLElement { node.addEventListener("dblclick", this.onNodeDblclick); // setup the children in a new node depth if (nodeData.children.length) { + // if we are at the display-depth don't iterate on the children + // simply mark that the nodes have children + if (depth == this.getAttribute("display-depth")){ + node.classList.add("non-final-leaf"); + return; + } // if the corresponding depth has not been added, do so now let depthDiv = this.shadowRoot.querySelector(`#depth-${depth}`); if (!depthDiv) { @@ -174,7 +176,7 @@ class Tree extends HTMLElement { } setupPaths(svg, nodeData){ - if (nodeData && this.attainedDepth < this.maxDepth) { + if (nodeData) { this.attainedDepth += 1; const parent = this.shadowRoot.querySelector(`#${nodeData.id}`); nodeData.children.forEach((childData) => { @@ -206,6 +208,7 @@ class Tree extends HTMLElement { const endX = endRect.left + (endRect.width / 2); + /** const line = document.createElementNS('http://www.w3.org/2000/svg', "line"); line.setAttribute("x1", `${startX}`); line.setAttribute("y1", `${startY}`); @@ -216,6 +219,33 @@ class Tree extends HTMLElement { line.setAttribute("data-start-node-id", startNode.id); line.setAttribute("data-end-node-id", endNode.id); svg.append(line); + **/ + // add a quadratic bezier curve path + let midX; + let controlX; + let controlSlope = 1; + if (endX < startX) { + midX = endX + 0.5 * (startX - endX); + controlX = midX + 0.5 * (startX - midX); + controlSlope *= -1; + } else { + midX = startX + 0.5 * (endX - startX); + controlX = startX + 0.5 * (startX - midX); + } + const midY = startY + 0.5 * (endY - startY); + let controlY = startY + 0.5 * (midY - startY); + controlX *= controlSlope; + controlY *= controlSlope; + const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); + path.setAttribute("stroke", "var(--palette-blue)"); + path.setAttribute("stroke-width", "3px"); + path.setAttribute("fill", "transparent"); + path.setAttribute("data-start-node-id", startNode.id); + path.setAttribute("data-end-node-id", endNode.id); + const d = `M ${startX} ${startY} Q ${controlX} ${controlY}, ${midX} ${midY} T ${endX} ${endY}`; + path.setAttribute("d", d); + svg.append(path); + } /** @@ -295,6 +325,16 @@ class Tree extends HTMLElement { wrapper.classList.add("animation-fade-out"); wrapper.replaceChildren(); } + + static get observedAttributes() { + return ["display-depth"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == "display-depth") { + this.setup(this.data); + } + } } window.customElements.define("tree-graph", Tree); diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index 7774ff424..8f9096307 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -39,7 +39,7 @@ return node; } - const data = treeMaker(5, 3); + const data = treeMaker(6, 3); console.log(data); document.addEventListener("DOMContentLoaded", () => { const tree = document.createElement("tree-graph"); From 0675f741bb35be45e7de0f7d7134b34981345aff Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 5 Dec 2022 22:16:02 +0100 Subject: [PATCH 30/57] fixing up connector lines in tree --- object_database/web/devtools/tree/Tree.js | 9 +++++---- object_database/web/devtools/tree/examples/tree.html | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 59bbe9e71..bfa4f1539 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -223,17 +223,18 @@ class Tree extends HTMLElement { // add a quadratic bezier curve path let midX; let controlX; + const midY = startY + 0.5 * (endY - startY); + let controlY = startY + 0.5 * (midY - startY); let controlSlope = 1; if (endX < startX) { midX = endX + 0.5 * (startX - endX); controlX = midX + 0.5 * (startX - midX); - controlSlope *= -1; + controlSlope = 1.02; } else { midX = startX + 0.5 * (endX - startX); - controlX = startX + 0.5 * (startX - midX); + controlX = startX + 0.5 * (midX - startX); + controlSlope = 0.98; } - const midY = startY + 0.5 * (endY - startY); - let controlY = startY + 0.5 * (midY - startY); controlX *= controlSlope; controlY *= controlSlope; const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index 8f9096307..8a573c1a3 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -39,7 +39,7 @@ return node; } - const data = treeMaker(6, 3); + const data = treeMaker(5, 4); console.log(data); document.addEventListener("DOMContentLoaded", () => { const tree = document.createElement("tree-graph"); From e784196b373b72eaab972d819f82cedf744c3674 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sat, 31 Dec 2022 14:41:58 +0100 Subject: [PATCH 31/57] updating id key for messages from CellHandler to devtools --- object_database/web/content/src/CellHandler.js | 2 +- object_database/web/devtools/background.js | 4 ++-- object_database/web/devtools/js/cell_panel.js | 2 ++ object_database/web/devtools/tree/examples/tree.html | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index a1f8b7cd5..2e2fcf930 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -520,7 +520,7 @@ class CellHandler { const buildTree = (cell) => { return { name: cell.constructor.name, - identity: cell.identity, + id: cell.identity, children: mapChildren(cell.namedChildren) } } diff --git a/object_database/web/devtools/background.js b/object_database/web/devtools/background.js index b97fc84c0..05a81972a 100644 --- a/object_database/web/devtools/background.js +++ b/object_database/web/devtools/background.js @@ -29,7 +29,7 @@ function connected(port) { // from the target window we forward this message to the panels // if the connection is not alive we log this in the devtools's // debugger console - notifyDevtoolsPabel(msg.data); + notifyDevtoolsPanel(msg.data); }); } // notify if the port has disconnected @@ -42,7 +42,7 @@ function connected(port) { chrome.runtime.onConnect.addListener(connected); -function notifyDevtoolsPabel(msg){ +function notifyDevtoolsPanel(msg){ if (portFromPanel){ portFromPanel.postMessage(msg); } else { diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index ff6810239..29411b46f 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -54,6 +54,8 @@ const cellsTreeDisplay = (cells) => { const tree = document.createElement("tree-graph"); const main = document.getElementById("main"); main.append(tree); + // displaying tree + console.log(cells); tree.setup(cells); } diff --git a/object_database/web/devtools/tree/examples/tree.html b/object_database/web/devtools/tree/examples/tree.html index 8a573c1a3..7d7e0a7c0 100644 --- a/object_database/web/devtools/tree/examples/tree.html +++ b/object_database/web/devtools/tree/examples/tree.html @@ -39,7 +39,7 @@ return node; } - const data = treeMaker(5, 4); + const data = treeMaker(10, 5); console.log(data); document.addEventListener("DOMContentLoaded", () => { const tree = document.createElement("tree-graph"); From 06620c1af30918933605559564badc3a6c1776f5 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sat, 31 Dec 2022 18:31:02 +0100 Subject: [PATCH 32/57] adding handling of DOM invalid cell/node ids prepending "node-" to each DOM element id keeping the original id in "data-original-id" attribute --- object_database/web/devtools/tree/Tree.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index bfa4f1539..3967471b4 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -140,7 +140,10 @@ class Tree extends HTMLElement { if(nodeData){ const node = document.createElement("tree-node"); node.name = nodeData.name; - node.setAttribute("id", nodeData.id); + // since the id might be a non-valid DOM element id such as int + // we prepend "node-" and keep the original id in a data attribute + node.setAttribute("id", `node-${nodeData.id}`); + node.setAttribute("data-original-id", nodeData.id); if (root) { node.setAttribute("data-root-node", true); } @@ -178,9 +181,9 @@ class Tree extends HTMLElement { setupPaths(svg, nodeData){ if (nodeData) { this.attainedDepth += 1; - const parent = this.shadowRoot.querySelector(`#${nodeData.id}`); + const parent = this.shadowRoot.querySelector(`#node-${nodeData.id}`); nodeData.children.forEach((childData) => { - const child = this.shadowRoot.querySelector(`#${childData.id}`); + const child = this.shadowRoot.querySelector(`#node-${childData.id}`); if (parent && child) { this.addSVGPath(svg, parent, child); this.setupPaths(svg, childData); @@ -265,7 +268,7 @@ class Tree extends HTMLElement { const subTree = this.findParentSubTree(id, this.data); this.setup(subTree, false); // do not cache this data } else { - const id = event.target.id; + const id = event.target.getAttribute("data-original-id"); const subTree = this.findSubTree(id, this.data); this.setup(subTree, false); // do not cache this data } @@ -275,7 +278,8 @@ class Tree extends HTMLElement { if (event.key == "ArrowUp") { // re-render the tree from the parent of the current root node const rootNode = this.shadowRoot.querySelector("tree-node[data-root-node]"); - const subTree = this.findParentSubTree(rootNode.id, this.data); + const rootNodeId = rootNode.getAttribute("data-original-id"); + const subTree = this.findParentSubTree(rootNodeId, this.data); this.setup(subTree, false); // do not cache this data } } From 17c8426ca6c0c999e231bfc4675807fcba3cc846 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 2 Jan 2023 12:38:43 +0100 Subject: [PATCH 33/57] addiung non-starting-root node handling fixing up navigation bugs --- object_database/web/devtools/tree/Tree.js | 33 ++++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 3967471b4..adb1a5807 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -64,7 +64,20 @@ svg { } .non-final-leaf { - background-color: var(--palette-lightblue) + background-color: var(--palette-lightblue); + cursor: pointer; + pointer-events: auto; +} + +.non-starting-root { + background-color: var(--palette-cyan); + cursor: pointer; + pointer-events: auto; + +} + +tree-node { + pointer-events: none; } @@ -113,7 +126,7 @@ class Tree extends HTMLElement { this.data = data; } const wrapper = this.shadowRoot.querySelector("#wrapper"); - wrapper.addEventListener("dblclick", this.onNodeDblclick); + // wrapper.addEventListener("dblclick", this.onNodeDblclick); const nodeDepth = document.createElement("div"); nodeDepth.classList.add("depth"); nodeDepth.setAttribute("id", "depth-0"); @@ -146,15 +159,22 @@ class Tree extends HTMLElement { node.setAttribute("data-original-id", nodeData.id); if (root) { node.setAttribute("data-root-node", true); + // if the node is not the root of entire tree + // mark it as such and add the dblclick event listener + // to allow up the tree navigation + if (nodeData.id !== this.data.id) { + node.classList.add("non-starting-root"); + node.addEventListener("dblclick", this.onNodeDblclick); + } } wrapperDiv.append(node); - node.addEventListener("dblclick", this.onNodeDblclick); // setup the children in a new node depth if (nodeData.children.length) { // if we are at the display-depth don't iterate on the children // simply mark that the nodes have children if (depth == this.getAttribute("display-depth")){ node.classList.add("non-final-leaf"); + node.addEventListener("dblclick", this.onNodeDblclick); return; } // if the corresponding depth has not been added, do so now @@ -264,7 +284,7 @@ class Tree extends HTMLElement { if (event.target.nodeName != "TREE-NODE") { this.setup(this.data); } else if (event.target.hasAttribute("data-root-node")) { - const id = event.target.id; + const id = event.target.getAttribute("data-original-id"); const subTree = this.findParentSubTree(id, this.data); this.setup(subTree, false); // do not cache this data } else { @@ -275,12 +295,17 @@ class Tree extends HTMLElement { } onKeyUp(event){ + event.preventDefault(); + event.stopPropagation(); if (event.key == "ArrowUp") { // re-render the tree from the parent of the current root node const rootNode = this.shadowRoot.querySelector("tree-node[data-root-node]"); const rootNodeId = rootNode.getAttribute("data-original-id"); const subTree = this.findParentSubTree(rootNodeId, this.data); this.setup(subTree, false); // do not cache this data + } else if (event.key == "Esc") { + // re-render from the starting root node + this.setup(this.data, false); } } /** From 098b492595e9883d90c0f2a462e715227a4984a4 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 2 Jan 2023 14:52:25 +0100 Subject: [PATCH 34/57] fixing up vertical paths in tree and adding on hover highlight when tree nodes are (almost) vertically aligned, use a basic svg path line instead of a bezier curve adding hover (mouseover mouseleave) handling on the target window object and corresponding no-op methods to tree setup these methods --- .../web/content/src/CellHandler.js | 29 +++++++ object_database/web/devtools/js/cell_panel.js | 17 +++- object_database/web/devtools/tree/Tree.js | 81 ++++++++++--------- 3 files changed, 90 insertions(+), 37 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 2e2fcf930..60df1f861 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -62,6 +62,7 @@ class CellHandler { // devtools related messages this.sendMessageToDevtools = this.sendMessageToDevtools.bind(this); this.updateDevtools = this.updateDevtools.bind(this); + this.setupDevtools = this.setupDevtools.bind(this); } tearDownAllLiveCells() { @@ -507,6 +508,34 @@ class CellHandler { } } + /** + * Setup utilities for devtools to call using chrome.devtools.inspectedWindow.eval() + * TODO: potentially this should pass via the content-scripts + */ + setupDevtools(){ + const = "cells-devtools-overlay"; + window.addDevtoolsHighlight = (id) => { + const cell = document.querySelector(`[data-cell-id="${id}"}]`); + const rect = cell.getBoundingClientRect(); + const overlay = document.createElement("div"); + overlay.style.position = "absolute"; + overlay.style.backgroundColor = "red"; + overlay.style.opacity = "0.5"; + overlay.style.left = rect.left +"px"; + overlay.style.top = rect.top + "px"; + overlay.style.height = rect.height + "px"; + overlay.style.width = rect.width + "px"; + overlay.setAttribute("id", overlayId); + document.body.append(overlay); + } + + window.removeDevtoolsHighlight = () => { + const overlay = document.getElementById(overlayId); + if (overlay) { + overlay.remove(); + } + } + } sendMessageToDevtools(msg){ // TODO perhaps this should be run by a worker msg.type = "cells_devtools"; diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 29411b46f..5af6db10b 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -54,8 +54,23 @@ const cellsTreeDisplay = (cells) => { const tree = document.createElement("tree-graph"); const main = document.getElementById("main"); main.append(tree); + // setup node hover event listeners + // NOTE: these are defined on window by CellHandler + tree.onNodeMouseover = (event) => { + // highlight the corresponding element in the target window + const id = event.target.getAttribute("data-original-id"); + chrome.devtools.inspectedWindow.eval( + `window.addDevtoolsHighlight(${id})` + ); + } + tree.onNodeMouseleave = (event) => { + // un-highlight the corresponding element in the target window + const id = event.target.getAttribute("data-original-id"); + chrome.devtools.inspectedWindow.eval( + `window.removeDevtoolsHighlight()` + ); + } // displaying tree - console.log(cells); tree.setup(cells); } diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index adb1a5807..aac45f915 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -103,6 +103,8 @@ class Tree extends HTMLElement { this.onWindowResize = this.onWindowResize.bind(this); // event handlers this.onNodeDblclick = this.onNodeDblclick.bind(this); + this.onNodeMouseover = this.onNodeMouseover.bind(this); + this.onNodeMouseleave = this.onNodeMouseleave.bind(this); this.onKeyUp = this.onKeyUp.bind(this); } @@ -157,6 +159,9 @@ class Tree extends HTMLElement { // we prepend "node-" and keep the original id in a data attribute node.setAttribute("id", `node-${nodeData.id}`); node.setAttribute("data-original-id", nodeData.id); + // add mouseover and leave event handlers (implemented outside of tree) + node.addEventListener("mouseover", this.onNodeMouseover); + node.addEventListener("mouseleave", this.onNodeMouseleave); if (root) { node.setAttribute("data-root-node", true); // if the node is not the root of entire tree @@ -222,6 +227,15 @@ class Tree extends HTMLElement { * @param {element} endNode */ addSVGPath(svg, startNode, endNode){ + // generic svg path attributes setup here + const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); + path.setAttribute("stroke", "var(--palette-blue)"); + path.setAttribute("stroke-width", "3px"); + path.setAttribute("fill", "transparent"); + path.setAttribute("data-start-node-id", startNode.id); + path.setAttribute("data-end-node-id", endNode.id); + + // calculate position here const startRect = startNode.getBoundingClientRect(); const startY = startRect.bottom; @@ -230,46 +244,33 @@ class Tree extends HTMLElement { const endY = endRect.top; const endX = endRect.left + (endRect.width / 2); - - /** - const line = document.createElementNS('http://www.w3.org/2000/svg', "line"); - line.setAttribute("x1", `${startX}`); - line.setAttribute("y1", `${startY}`); - line.setAttribute("x2", `${endX}`); - line.setAttribute("y2", `${endY}`); - line.setAttribute("stroke", "var(--palette-blue)"); - line.setAttribute("stroke-width", "3px"); - line.setAttribute("data-start-node-id", startNode.id); - line.setAttribute("data-end-node-id", endNode.id); - svg.append(line); - **/ - // add a quadratic bezier curve path - let midX; - let controlX; - const midY = startY + 0.5 * (endY - startY); - let controlY = startY + 0.5 * (midY - startY); - let controlSlope = 1; - if (endX < startX) { - midX = endX + 0.5 * (startX - endX); - controlX = midX + 0.5 * (startX - midX); - controlSlope = 1.02; + let d; // this is the path data + if ( Math.abs(endX - startX) < 5) { + // draw a straight vertical line, ie the two nodes + // are on top of each other + d = `M ${startX} ${startY} L ${endX} ${endY}`; } else { - midX = startX + 0.5 * (endX - startX); - controlX = startX + 0.5 * (midX - startX); - controlSlope = 0.98; + // add a quadratic bezier curve path + let midX; + let controlX; + const midY = startY + 0.5 * (endY - startY); + let controlY = startY + 0.5 * (midY - startY); + let controlSlope = 1; + if (endX < startX) { + midX = endX + 0.5 * (startX - endX); + controlX = midX + 0.5 * (startX - midX); + controlSlope = 1.02; + } else { + midX = startX + 0.5 * (endX - startX); + controlX = startX + 0.5 * (midX - startX); + controlSlope = 0.98; + } + controlX *= controlSlope; + controlY *= controlSlope; + d = `M ${startX} ${startY} Q ${controlX} ${controlY}, ${midX} ${midY} T ${endX} ${endY}`; } - controlX *= controlSlope; - controlY *= controlSlope; - const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); - path.setAttribute("stroke", "var(--palette-blue)"); - path.setAttribute("stroke-width", "3px"); - path.setAttribute("fill", "transparent"); - path.setAttribute("data-start-node-id", startNode.id); - path.setAttribute("data-end-node-id", endNode.id); - const d = `M ${startX} ${startY} Q ${controlX} ${controlY}, ${midX} ${midY} T ${endX} ${endY}`; path.setAttribute("d", d); svg.append(path); - } /** @@ -308,6 +309,14 @@ class Tree extends HTMLElement { this.setup(this.data, false); } } + + onNodeMouseover(event) { + // no-op + } + + onNodeMouseleave(event) { + // no-op + } /** * I recursively walk the tree to find the corresponding * node, and when I do I return its subtree From 1d35ea0fd79c108f84470b10ffcf2480e99cf4a0 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 2 Jan 2023 14:57:26 +0100 Subject: [PATCH 35/57] adding forgotten setupDevtools() in CellHandler --- object_database/web/content/src/CellHandler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 60df1f861..f4edb7c89 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -132,6 +132,7 @@ class CellHandler { ]) ]) ); + this.setupDevtools(); this.sendMessageToDevtools({ status: "initial load" }); From 580767f94a2c3955dd8a4ea2f6a53963558e0f68 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 2 Jan 2023 14:59:12 +0100 Subject: [PATCH 36/57] typo fix --- object_database/web/content/src/CellHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index f4edb7c89..b9a39ac02 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -514,7 +514,7 @@ class CellHandler { * TODO: potentially this should pass via the content-scripts */ setupDevtools(){ - const = "cells-devtools-overlay"; + const overlayId = "cells-devtools-overlay"; window.addDevtoolsHighlight = (id) => { const cell = document.querySelector(`[data-cell-id="${id}"}]`); const rect = cell.getBoundingClientRect(); From 9006db80847aa3fb0219525715189ee0fa0f22d0 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 2 Jan 2023 15:07:23 +0100 Subject: [PATCH 37/57] hightlight query selector fix --- object_database/web/content/src/CellHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index b9a39ac02..08549733d 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -516,7 +516,7 @@ class CellHandler { setupDevtools(){ const overlayId = "cells-devtools-overlay"; window.addDevtoolsHighlight = (id) => { - const cell = document.querySelector(`[data-cell-id="${id}"}]`); + const cell = document.querySelector(`[data-cell-id="${id}"]`); const rect = cell.getBoundingClientRect(); const overlay = document.createElement("div"); overlay.style.position = "absolute"; From 6db1ea7b89479480aa6836c678bb67450184c842 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 2 Jan 2023 15:11:34 +0100 Subject: [PATCH 38/57] adding better overlay cleanup for devtools --- object_database/web/content/src/CellHandler.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 08549733d..01aa1ff4c 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -531,10 +531,8 @@ class CellHandler { } window.removeDevtoolsHighlight = () => { - const overlay = document.getElementById(overlayId); - if (overlay) { - overlay.remove(); - } + const overlays = document.querySelectorAll(`#${overlayId}`); + overlays.forEach((el) => el.remove()); } } sendMessageToDevtools(msg){ From 35b3cb1b9001cb20dc1fe167ff7dd5d7d6be2114 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 2 Jan 2023 15:21:15 +0100 Subject: [PATCH 39/57] adding better hover devtools highlighting and related changes --- object_database/web/devtools/tree/Tree.js | 26 +++++++++-------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index aac45f915..5f2c29fe9 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -66,20 +66,14 @@ svg { .non-final-leaf { background-color: var(--palette-lightblue); cursor: pointer; - pointer-events: auto; } .non-starting-root { background-color: var(--palette-cyan); cursor: pointer; - pointer-events: auto; } -tree-node { - pointer-events: none; -} -
`; @@ -282,16 +276,16 @@ class Tree extends HTMLElement { onNodeDblclick(event){ // if clicking on the root node, reset back to cached this.data tree - if (event.target.nodeName != "TREE-NODE") { - this.setup(this.data); - } else if (event.target.hasAttribute("data-root-node")) { - const id = event.target.getAttribute("data-original-id"); - const subTree = this.findParentSubTree(id, this.data); - this.setup(subTree, false); // do not cache this data - } else { - const id = event.target.getAttribute("data-original-id"); - const subTree = this.findSubTree(id, this.data); - this.setup(subTree, false); // do not cache this data + if (event.target.nodeName == "TREE-NODE") { + if (event.target.hasAttribute("data-root-node")) { + const id = event.target.getAttribute("data-original-id"); + const subTree = this.findParentSubTree(id, this.data); + this.setup(subTree, false); // do not cache this data + } else { + const id = event.target.getAttribute("data-original-id"); + const subTree = this.findSubTree(id, this.data); + this.setup(subTree, false); // do not cache this data + } } } From d8871ce3004069a57f2e0ba0a0f1cafaa4a4ced2 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Tue, 24 Jan 2023 16:08:41 +0100 Subject: [PATCH 40/57] filtering out non cells from messager to devtools in CellHandler --- .../web/content/src/CellHandler.js | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 01aa1ff4c..abb267a3b 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -546,10 +546,12 @@ class CellHandler { **/ updateDevtools(){ const buildTree = (cell) => { - return { - name: cell.constructor.name, - id: cell.identity, - children: mapChildren(cell.namedChildren) + if (cell.isCell) { + return { + name: cell.constructor.name, + id: cell.identity, + children: mapChildren(cell.namedChildren) + } } } // NOTE: sometimes a named child is a cell, sometimes it's an array of cells @@ -561,10 +563,16 @@ class CellHandler { if (child){ if (child instanceof Array){ child.forEach((subchild) => { - children.push(buildTree(subchild)); + const subTree = buildTree(subchild); + if (subTree){ + children.push(buildTree(subchild)); + } }) } - children.push(buildTree(child)); + const subTree = buildTree(child); + if (subTree) { + children.push(buildTree(child)); + } } }) } From f04608c14d4c00422e8c4086690f7bf6aeb02d2c Mon Sep 17 00:00:00 2001 From: dkrasner Date: Wed, 25 Jan 2023 12:12:19 +0100 Subject: [PATCH 41/57] removing old d3 tree js file --- object_database/web/devtools/js/tree.js | 263 ------------------------ 1 file changed, 263 deletions(-) delete mode 100644 object_database/web/devtools/js/tree.js diff --git a/object_database/web/devtools/js/tree.js b/object_database/web/devtools/js/tree.js deleted file mode 100644 index a8ac2594f..000000000 --- a/object_database/web/devtools/js/tree.js +++ /dev/null @@ -1,263 +0,0 @@ - -class CellsTree extends Object { - constructor(data) { - super(); - - this.data = data; - - // basic view settings - // TODO: maybe these should be passed as params to the constructor - this.duration = 750; - this.rectW = 60; - this.rectH=30; - - // bound methods - this.setupTree = this.setupTree.bind(this); - this.update = this.update.bind(this); - this.collapse = this.collapse.bind(this); - this.redraw = this.redraw.bind(this); - this.onDblclick = this.onDblclick.bind(this); - this.onClick = this.onClick.bind(this); - this.onMouseover = this.onMouseover.bind(this); - this.onMouseleave = this.onMouseleave.bind(this); - } - - setupTree(){ - this.tree = d3.layout.tree().nodeSize([70, 40]); - const zm = d3.behavior.zoom() - .scaleExtent([1,3]) - .on("zoom", this.redraw) - // the main svg container for the tree - this.svg = d3.select("#main").append("svg") - .attr("width", "100%") - .attr("height", 1000) - .call(zm) - .append("g") - .attr( - "transform", - `translate(${350},${20})` - ); - //necessary so that zoom knows where to zoom and unzoom from - zm.translate([350, 20]); - - this.data.x0 = 0; - this.data.y0 = 700 / 2; - - // collapse all children for now - // TODO do we want this? - this.data.children.forEach(this.collapse); - - // build the tree - this.update(this.data); - } - - update(source) { - // need to define these in method scope since a number of the - // callbacks are not bound to the class. TODO - let id = 0; - const rectW = this.rectW; - const rectH = this.rectH; - - const diagonal = d3.svg.diagonal() - .projection(function (d) { - return [d.x + rectW / 2, d.y + rectH / 2]; - }); - // Compute the new tree layout. - const nodes = this.tree.nodes(this.data).reverse(); - const links = this.tree.links(nodes); - - // Normalize for fixed-depth. - nodes.forEach(function (d) { - d.y = d.depth * 180; - }); - - // Update the nodes… - const node = this.svg.selectAll("g.node") - .data(nodes, function (d) { - return d.id = ++id; - } - ); - - // Enter any new nodes at the parent's previous position. - const nodeEnter = node.enter().append("g") - .attr("class", "node") - .attr("transform", function (d) { - return `translate(${source.x0},${source.y0})`; - }) - .on("dblclick", this.onDblclick) - .on("click", this.onClick) - .on("mouseover", this.onMouseover) - .on("mouseleave", this.onMouseleave); - - - nodeEnter.append("rect") - .attr("width", this.rectW) - .attr("height", this.rectH) - .attr("stroke", "black") - .attr("stroke-width", 1) - .style("fill", function (d) { - return d._children ? "lightsteelblue" : "#fff"; - } - ); - - nodeEnter.append("text") - .attr("x", this.rectW / 2) - .attr("y", this.rectH / 2) - .attr("dy", ".35em") - .attr("text-anchor", "middle") - .text(function (d) { - // limit the name to 7 chars since text-overflow - // doesn't seem to work here - let name = d.name; - if (name.length > 7){ - name = name.slice(0, 7) + "..."; - } - return name; - } - ); - - // Transition nodes to their new position. - const nodeUpdate = node.transition() - .duration(this.duration) - .attr("transform", function (d) { - return `translate(${d.x},${d.y})`; - } - ); - - nodeUpdate.select("rect") - .attr("width", this.rectW) - .attr("height", this.rectH) - .attr("stroke", "black") - .attr("stroke-width", 1) - .style("fill", function (d) { - return d._children ? "lightsteelblue" : "#fff"; - } - ); - - nodeUpdate.select("text") - .style("fill-opacity", 1); - - // Transition exiting nodes to the parent's new position. - const nodeExit = node.exit().transition() - .duration(this.duration) - .attr("transform", function (d) { - return `translate(${source.x},${source.y})`; - } - ).remove(); - - nodeExit.select("rect") - .attr("width", this.rectW) - .attr("height", this.rectH) - .attr("stroke", "black") - .attr("stroke-width", 1); - - nodeExit.select("text"); - - // Update the links… - const link = this.svg.selectAll("path.link") - .data(links, function (d) { - return d.target.id; - } - ); - - // Enter any new links at the parent's previous position. - link.enter().insert("path", "g") - .attr("class", "link") - .attr("x", this.rectW / 2) - .attr("y", this.rectH / 2) - .attr("d", function (d) { - const o = { - x: source.x0, - y: source.y0 - }; - return diagonal({ - source: o, - target: o - }); - }); - - // Transition links to their new position. - link.transition() - .duration(this.duration) - .attr("d", diagonal); - - // Transition exiting nodes to the parent's new position. - link.exit().transition() - .duration(this.duration) - .attr("d", function (d) { - const o = { - x: source.x, - y: source.y - }; - return diagonal({ - source: o, - target: o - }); - }) - .remove(); - - // Stash the old positions for transition. - nodes.forEach(function (d) { - d.x0 = d.x; - d.y0 = d.y; - }); - } - - collapse(d) { - if (d.children) { - d._children = d.children.slice(); - d._children.forEach(this.collapse); - d.children = null; - } - } - - - // Toggle children on click. - onDblclick(d) { - // prevent the default zooming in/out behavior - d3.event.stopPropagation(); - if (d.children) { - d._children = d.children.slice(); - d.children = null; - } else { - d.children = d._children.slice(); - d._children = null; - } - this.update(d); - } - - onClick(event){ - // update the cell data - // probably should be handled by a different class - const infoDiv = document.getElementById("cell-info") - infoDiv.textContent = `${event.name} (id: ${event.identity})`; - } - - onMouseover(event){ - // highlighte the corresponding element in the target window - chrome.devtools.inspectedWindow.eval( - `document.querySelector("[data-cell-id='${event.identity}']").style.backgroundColor = 'lightblue'` - ); - } - - onMouseleave(event){ - console.log(event); - // highlighte the corresponding element in the target window - chrome.devtools.inspectedWindow.eval( - `document.querySelector("[data-cell-id='${event.identity}']").style.backgroundColor= 'initial'` - ); - } - //Redraw for zoom - redraw() { - this.svg.attr("transform", - `translate(${d3.event.translate})` - +`scale(${d3.event.scale})` - ); - } -} - - -export { - CellsTree, - CellsTree as default -} From 9b0227b3e910bdb52b5799d9ad7d412d1eb83cbb Mon Sep 17 00:00:00 2001 From: dkrasner Date: Wed, 25 Jan 2023 15:16:06 +0100 Subject: [PATCH 42/57] adding node click and customization handlers click handler display node data in the info panel customization performs additional highlighting --- object_database/web/devtools/js/cell_panel.js | 25 +++++++++++++++++++ object_database/web/devtools/tree/Tree.js | 13 +++++++++- object_database/web/devtools/tree/TreeNode.js | 5 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 5af6db10b..2b75ae0c6 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -44,6 +44,18 @@ const reconnectingDisplay = () => { main.textContent = "Reconnecting: no cells loaded"; } +const updateInfoPanel = (node) => { + const infoPanel = document.getElementById("cell-info"); + const id = node.getAttribute("data-original-id"); + let info = `cell-id: ${id}`; + const tree = document.querySelector("tree-graph"); + const parentSubtree = tree.findParentSubTree(id, tree.data); + if (parentSubtree.name.match("Subscribed")) { + info = `${info}\nsubscribed`; + } + infoPanel.innerText = info; +} + const cellsTreeDisplay = (cells) => { clearDisplay(); // init and run @@ -54,6 +66,7 @@ const cellsTreeDisplay = (cells) => { const tree = document.createElement("tree-graph"); const main = document.getElementById("main"); main.append(tree); + tree.setAttribute("display-depth", 4); // setup node hover event listeners // NOTE: these are defined on window by CellHandler tree.onNodeMouseover = (event) => { @@ -70,6 +83,18 @@ const cellsTreeDisplay = (cells) => { `window.removeDevtoolsHighlight()` ); } + + tree.onNodeClick = (event) => { + updateInfoPanel(event.target); + } + tree.customizeNode = (node) => { + if (node.name == "Subscribed") { + node.style.backgroundColor = "var(--palette-beige)"; + } + // customize a tooltip here + const id = node.getAttribute("data-original-id"); + node.title = `cell-id: ${id}`; + } // displaying tree tree.setup(cells); } diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 5f2c29fe9..8cd6b7ec9 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -71,7 +71,6 @@ svg { .non-starting-root { background-color: var(--palette-cyan); cursor: pointer; - } @@ -95,10 +94,12 @@ class Tree extends HTMLElement { this.setupPaths = this.setupPaths.bind(this); this.addSVGPath = this.addSVGPath.bind(this); this.onWindowResize = this.onWindowResize.bind(this); + this.customizeNode = this.customizeNode; // event handlers this.onNodeDblclick = this.onNodeDblclick.bind(this); this.onNodeMouseover = this.onNodeMouseover.bind(this); this.onNodeMouseleave = this.onNodeMouseleave.bind(this); + this.onNodeClick = this.onNodeClick.bind(this); this.onKeyUp = this.onKeyUp.bind(this); } @@ -115,6 +116,10 @@ class Tree extends HTMLElement { document.removeEventListener("key", this.onKeyUp); } + customizeNode(node){ + //Noop, to be used by consumers + } + setup(data, cache=true){ this.clear(); if (cache){ @@ -156,6 +161,7 @@ class Tree extends HTMLElement { // add mouseover and leave event handlers (implemented outside of tree) node.addEventListener("mouseover", this.onNodeMouseover); node.addEventListener("mouseleave", this.onNodeMouseleave); + node.addEventListener("click", this.onNodeClick); if (root) { node.setAttribute("data-root-node", true); // if the node is not the root of entire tree @@ -166,6 +172,7 @@ class Tree extends HTMLElement { node.addEventListener("dblclick", this.onNodeDblclick); } } + this.customizeNode(node); wrapperDiv.append(node); // setup the children in a new node depth if (nodeData.children.length) { @@ -311,6 +318,10 @@ class Tree extends HTMLElement { onNodeMouseleave(event) { // no-op } + + onNodeClick(event) { + // no-op + } /** * I recursively walk the tree to find the corresponding * node, and when I do I return its subtree diff --git a/object_database/web/devtools/tree/TreeNode.js b/object_database/web/devtools/tree/TreeNode.js index 97182fcd3..941c466c1 100644 --- a/object_database/web/devtools/tree/TreeNode.js +++ b/object_database/web/devtools/tree/TreeNode.js @@ -30,6 +30,7 @@ div#wrapper { class TreeNode extends HTMLElement { constructor() { super(); + this.template = document.createElement("template"); this.template.innerHTML = templateString; this.attachShadow({ mode: "open" }); @@ -39,7 +40,11 @@ class TreeNode extends HTMLElement { set name(s){ const name = this.shadowRoot.querySelector("#name"); name.innerText = s; + } + get name() { + const name = this.shadowRoot.querySelector("#name"); + return name.innerText; } } From 4de7667c084bd3d2b6f982eace1edfc56fe5d4b7 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 1 May 2023 16:20:06 +0200 Subject: [PATCH 43/57] improving the bezier curves connecting trees --- object_database/web/devtools/tree/Tree.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/object_database/web/devtools/tree/Tree.js b/object_database/web/devtools/tree/Tree.js index 8cd6b7ec9..d30c7419f 100644 --- a/object_database/web/devtools/tree/Tree.js +++ b/object_database/web/devtools/tree/Tree.js @@ -246,14 +246,29 @@ class Tree extends HTMLElement { const endX = endRect.left + (endRect.width / 2); let d; // this is the path data - if ( Math.abs(endX - startX) < 5) { + if ( Math.abs(endX - startX) < 100) { // draw a straight vertical line, ie the two nodes // are on top of each other d = `M ${startX} ${startY} L ${endX} ${endY}`; } else { // add a quadratic bezier curve path let midX; + const midY = startY + 0.5 * (endY - startY); let controlX; + let controlY; + if (endX < startX) { + midX = endX + 0.5 * (startX - endX); + controlX = midX + 0.5 * (startX - midX); + } else { + midX = startX + 0.5 * (endX - startX); + controlX = startX + 0.5 * (midX - startX); + } + // the controlLine is perpendicular to the line which connects + // the start to the end points + const clSlope = -1 * (endY - startY) / (endX - startX); + const clYInt = midY - clSlope * midX; + controlY = clSlope * controlX + clYInt; + /* const midY = startY + 0.5 * (endY - startY); let controlY = startY + 0.5 * (midY - startY); let controlSlope = 1; @@ -268,6 +283,9 @@ class Tree extends HTMLElement { } controlX *= controlSlope; controlY *= controlSlope; + */ + console.log("new controls") + d = `M ${startX} ${startY} Q ${controlX} ${controlY}, ${midX} ${midY} T ${endX} ${endY}`; } path.setAttribute("d", d); From a6ee429e9f8c60438780506039471d37c5d9f3cc Mon Sep 17 00:00:00 2001 From: dkrasner Date: Wed, 3 May 2023 16:56:38 +0200 Subject: [PATCH 44/57] updating cell info panel --- object_database/web/content/src/CellHandler.js | 2 +- object_database/web/devtools/js/cell_panel.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index abb267a3b..2cbed4f53 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -520,7 +520,7 @@ class CellHandler { const rect = cell.getBoundingClientRect(); const overlay = document.createElement("div"); overlay.style.position = "absolute"; - overlay.style.backgroundColor = "red"; + overlay.style.backgroundColor = "#cec848"; overlay.style.opacity = "0.5"; overlay.style.left = rect.left +"px"; overlay.style.top = rect.top + "px"; diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 2b75ae0c6..21cb09a96 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -53,6 +53,14 @@ const updateInfoPanel = (node) => { if (parentSubtree.name.match("Subscribed")) { info = `${info}\nsubscribed`; } + const nodeTree = parentSubtree.children.filter((n) => { + return n.id = node.id; + })[0] + let childIds = ""; + nodeTree.children.forEach((c) => { + childIds = `${childIds}, ${c.id}`; + }); + info = `${info}\nchild node ids: ${childIds}`; infoPanel.innerText = info; } @@ -78,7 +86,6 @@ const cellsTreeDisplay = (cells) => { } tree.onNodeMouseleave = (event) => { // un-highlight the corresponding element in the target window - const id = event.target.getAttribute("data-original-id"); chrome.devtools.inspectedWindow.eval( `window.removeDevtoolsHighlight()` ); From 71d0049a4c7d1fb70a77a578b345a4491f3ff7a8 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Thu, 4 May 2023 22:27:53 +0200 Subject: [PATCH 45/57] temp removal of child node display in info panel looks like a weird race condition --- object_database/web/devtools/js/cell_panel.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 21cb09a96..7b26b5670 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -53,6 +53,7 @@ const updateInfoPanel = (node) => { if (parentSubtree.name.match("Subscribed")) { info = `${info}\nsubscribed`; } + /* const nodeTree = parentSubtree.children.filter((n) => { return n.id = node.id; })[0] @@ -61,6 +62,7 @@ const updateInfoPanel = (node) => { childIds = `${childIds}, ${c.id}`; }); info = `${info}\nchild node ids: ${childIds}`; + */ infoPanel.innerText = info; } From 6de8bd82f9aa79ce4434d737a8bdeb9949f27b29 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Thu, 25 May 2023 11:38:09 +0200 Subject: [PATCH 46/57] minor clean and improvement of cells panel --- object_database/web/devtools/cells_panel.css | 3 +++ object_database/web/devtools/js/cell_panel.js | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/object_database/web/devtools/cells_panel.css b/object_database/web/devtools/cells_panel.css index eff198b16..369acfdb6 100644 --- a/object_database/web/devtools/cells_panel.css +++ b/object_database/web/devtools/cells_panel.css @@ -22,6 +22,9 @@ div#cell-info { justify-content: center; align-items: center; min-width: 30%; + text-align: center; + font-family: Roboto; + font-size: 20px; } .node { diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 7b26b5670..2b2b34a0e 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -47,11 +47,12 @@ const reconnectingDisplay = () => { const updateInfoPanel = (node) => { const infoPanel = document.getElementById("cell-info"); const id = node.getAttribute("data-original-id"); - let info = `cell-id: ${id}`; + const name = node.name; + let info = `${name}\ncell-id: ${id}`; const tree = document.querySelector("tree-graph"); const parentSubtree = tree.findParentSubTree(id, tree.data); if (parentSubtree.name.match("Subscribed")) { - info = `${info}\nsubscribed`; + info = `${info}\nsubscribed to cell-id: ${parentSubtree.id}`; } /* const nodeTree = parentSubtree.children.filter((n) => { From 80274dfc9de43db59c05585f51e66cc603dd611a Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 26 May 2023 12:58:38 +0200 Subject: [PATCH 47/57] adding devtools request for cells source code --- object_database/web/cells/cells.py | 16 ++++++++++++++++ object_database/web/content/src/CellHandler.js | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/object_database/web/cells/cells.py b/object_database/web/cells/cells.py index b555c3c45..b363ad8dc 100644 --- a/object_database/web/cells/cells.py +++ b/object_database/web/cells/cells.py @@ -17,6 +17,9 @@ import traceback import logging import types +import textwrap + +from inspect import getsourcelines from object_database.web.cells.session_state import SessionState from object_database.web.cells.cell import Cell @@ -127,6 +130,19 @@ def onMessage(self, message): if cell is not None: cell.mostRecentFocusId = self.focusEventId + if message.get("event") == "devtoolsRequest": + cellId = message.get("cellId") + cell = self._cells.get(cellId) + data = {} + if message.get("request") == "source": + sourceString = textwrap.dedent( + "".join(getsourcelines(cell.__func__)[0]) + ) + data["source"] = sourceString + print("GETTING DEVTOOLS MESSAGE") + print("response %s" % data) + return data + def _executeCallback(self, callback, withLogging=True): context = DependencyContext(self, readOnly=False) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 2cbed4f53..545cf06df 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -547,6 +547,13 @@ class CellHandler { updateDevtools(){ const buildTree = (cell) => { if (cell.isCell) { + // get the source code for the cell + const msg = { + event: "devtoolsRequest", + request: "source", + cellId: cell.identity + } + this.sendMessageToCells(msg); return { name: cell.constructor.name, id: cell.identity, From 29f033014b735414572a7d4ca111ca883b9617b0 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 26 May 2023 13:04:27 +0200 Subject: [PATCH 48/57] adding print statement to cell --- object_database/web/cells/cells.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/object_database/web/cells/cells.py b/object_database/web/cells/cells.py index b363ad8dc..9fa741060 100644 --- a/object_database/web/cells/cells.py +++ b/object_database/web/cells/cells.py @@ -135,13 +135,16 @@ def onMessage(self, message): cell = self._cells.get(cellId) data = {} if message.get("request") == "source": + print("GETTING DEVTOOLS MESSAGE") + print(cell) + """ sourceString = textwrap.dedent( "".join(getsourcelines(cell.__func__)[0]) ) data["source"] = sourceString - print("GETTING DEVTOOLS MESSAGE") print("response %s" % data) return data + """ def _executeCallback(self, callback, withLogging=True): From 3a80632feb63be69e350032f85a057647980439c Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 26 May 2023 14:23:56 +0200 Subject: [PATCH 49/57] pausing cells messages --- object_database/web/cells/cells.py | 1 + object_database/web/content/src/CellHandler.js | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/object_database/web/cells/cells.py b/object_database/web/cells/cells.py index 9fa741060..7a762c6b8 100644 --- a/object_database/web/cells/cells.py +++ b/object_database/web/cells/cells.py @@ -137,6 +137,7 @@ def onMessage(self, message): if message.get("request") == "source": print("GETTING DEVTOOLS MESSAGE") print(cell) + print(type(cell)) """ sourceString = textwrap.dedent( "".join(getsourcelines(cell.__func__)[0]) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 545cf06df..2cbed4f53 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -547,13 +547,6 @@ class CellHandler { updateDevtools(){ const buildTree = (cell) => { if (cell.isCell) { - // get the source code for the cell - const msg = { - event: "devtoolsRequest", - request: "source", - cellId: cell.identity - } - this.sendMessageToCells(msg); return { name: cell.constructor.name, id: cell.identity, From f631598cc648d6705baa8b818a4bfe59fd770f4f Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 26 May 2023 14:43:08 +0200 Subject: [PATCH 50/57] setting up target window call to get cell source --- object_database/web/content/src/CellHandler.js | 8 ++++++++ object_database/web/devtools/js/cell_panel.js | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 2cbed4f53..af8bf1e66 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -534,6 +534,14 @@ class CellHandler { const overlays = document.querySelectorAll(`#${overlayId}`); overlays.forEach((el) => el.remove()); } + + window.sendCellSource = (id) => { + this.sendMessageToCells({ + event: "devtoolsRequest", + request: "source", + cellId: id + }); + } } sendMessageToDevtools(msg){ // TODO perhaps this should be run by a worker diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 2b2b34a0e..3d21c9965 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -47,6 +47,10 @@ const reconnectingDisplay = () => { const updateInfoPanel = (node) => { const infoPanel = document.getElementById("cell-info"); const id = node.getAttribute("data-original-id"); + // we need to retrieve the source code for the node + chrome.devtools.inspectedWindow.eval( + `window.sendCellSource(${id})` + ); const name = node.name; let info = `${name}\ncell-id: ${id}`; const tree = document.querySelector("tree-graph"); From 7e5b8a57dd2e7c94ff15eb4bf0d7efd7a8832b07 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 26 May 2023 15:00:04 +0200 Subject: [PATCH 51/57] fixing up cell lookup --- object_database/web/cells/cells.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/object_database/web/cells/cells.py b/object_database/web/cells/cells.py index 7a762c6b8..c0a597fbf 100644 --- a/object_database/web/cells/cells.py +++ b/object_database/web/cells/cells.py @@ -131,13 +131,12 @@ def onMessage(self, message): if cell is not None: cell.mostRecentFocusId = self.focusEventId if message.get("event") == "devtoolsRequest": - cellId = message.get("cellId") + cellId = str(message.get("cellId")) cell = self._cells.get(cellId) data = {} if message.get("request") == "source": print("GETTING DEVTOOLS MESSAGE") print(cell) - print(type(cell)) """ sourceString = textwrap.dedent( "".join(getsourcelines(cell.__func__)[0]) From bf7b43c14e273439a0fa0eade8a9e8ab172a6d98 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Thu, 1 Jun 2023 15:43:03 +0200 Subject: [PATCH 52/57] adding a quick install and a readme --- README.md | 3 ++- quick_install.sh | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 quick_install.sh diff --git a/README.md b/README.md index 97a92a9aa..a66017521 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ from the repo. The public PyPI release is out of date. ```shell pip install -e . ``` - + +(there is a [quick_install.sh])(./quick_install.sh) file for you) # Major components This repo has 3 major components and 1 notable major dependency: diff --git a/quick_install.sh b/quick_install.sh new file mode 100644 index 000000000..5f26b45b9 --- /dev/null +++ b/quick_install.sh @@ -0,0 +1,13 @@ +git clone https://github.com/APrioriInvestments/object_database +apt-get update +apt-get install gcc +apt-get install python3-dev && apt-get install libssl-dev +apt-get update && sudo apt-get install --reinstall build-essential +cd object_database +python3 -m venv .venv +. .venv/bin/activate +export PYTHONPATH=`pwd` +pip install -e . +pip install flask-sockets +pip install urllib3==1.25.4 +object_database_webtest From 74f6b8fd72d2139217e90d93723d2cc59971a6ec Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sun, 11 Jun 2023 10:00:18 +0000 Subject: [PATCH 53/57] testing setting panel to background communication in devtools --- object_database/web/devtools/devtools_init.js | 4 ++-- object_database/web/devtools/js/cell_panel.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/object_database/web/devtools/devtools_init.js b/object_database/web/devtools/devtools_init.js index a9073b3f1..05cbb10d1 100644 --- a/object_database/web/devtools/devtools_init.js +++ b/object_database/web/devtools/devtools_init.js @@ -12,7 +12,7 @@ chrome.devtools.panels.create( const portFromPanel = chrome.runtime.connect({name: "port-from-panel"}); portFromPanel.onMessage.addListener(function(msg) { if (_window){ - // handleMessageFromBackground() is defined in panel.js + // handleMessageFromBackground() is defined in cell_panel.js _window.handleMessageFromBackground(msg); } else { console.log("no connection to background"); @@ -42,7 +42,7 @@ chrome.devtools.panels.create( } // If we ever need to send messages back via the port // we can do that as below - _window.respond = function(msg) { + _window.sendMessageToBackground = function(msg) { portFromPanel.postMessage(msg); } }); diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index 3d21c9965..ee7dd9926 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -58,6 +58,9 @@ const updateInfoPanel = (node) => { if (parentSubtree.name.match("Subscribed")) { info = `${info}\nsubscribed to cell-id: ${parentSubtree.id}`; } + + console.log("Clicked on node, sending message to background") + window.sendMessageToBackground("I need more data here"); /* const nodeTree = parentSubtree.children.filter((n) => { return n.id = node.id; From 931cd270ef5a887ecffb61c74d9a11b89b5e5ae8 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Fri, 16 Jun 2023 19:40:07 +0200 Subject: [PATCH 54/57] cleaning up ports in devtools cleaner setup for content-script <-> background background <-> panel/devtools window port API --- object_database/web/devtools/background.js | 35 +++++++++++-------- .../web/devtools/content-script.js | 8 ++--- object_database/web/devtools/devtools_init.js | 15 +++++--- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/object_database/web/devtools/background.js b/object_database/web/devtools/background.js index 05a81972a..7e56bad14 100644 --- a/object_database/web/devtools/background.js +++ b/object_database/web/devtools/background.js @@ -8,23 +8,26 @@ * respectively. Here we listen for these connection and create * connection/port specific handlers. */ -var portFromCS; -var portFromPanel; +var portCSBackground; +var portPanelBackground; function connected(port) { // handle all communication to and from the panel - if (port.name === "port-from-panel"){ - portFromPanel = port; + if (port.name === "port-panel-background"){ + portPanelBackground = port; // at the moment we don't do anything with messages coming // from the panels - portFromPanel.onMessage.addListener(function(msg) { + portPanelBackground.onMessage.addListener(function(msg) { console.log("recieved message from panel", msg); + if (msg.action == "notifyCS") { + notifyCS(msg); + } }); }; // handle all communication to and from the content script - if (port.name === "port-from-cs"){ - portFromCS = port; - portFromCS.onMessage.addListener(function(msg) { + if (port.name === "port-cs-background"){ + portCSBackground = port; + portCSBackground.onMessage.addListener(function(msg) { // Having received a message from the content script, i.e. // from the target window we forward this message to the panels // if the connection is not alive we log this in the devtools's @@ -34,7 +37,7 @@ function connected(port) { } // notify if the port has disconnected port.onDisconnect.addListener(function(port) { - if (port.name === "port-from-panel" || port.name === "port-from-cs"){ + if (port.name === "port-panel-background" || port.name === "port-cs-background"){ console.log(`${port.name} has disconnected`); }; }); @@ -43,14 +46,18 @@ function connected(port) { chrome.runtime.onConnect.addListener(connected); function notifyDevtoolsPanel(msg){ - if (portFromPanel){ - portFromPanel.postMessage(msg); + if (portPanelBackground){ + portPanelBackground.postMessage(msg); } else { console.log(msg); console.log("failed to send message to devtools panel: port disconnected"); } } -// chrome.browserAction.onClicked.addListener(function() { -// portFromCS.postMessage({greeting: "they clicked the button!"}); -// }); +function notifyCS(msg) { + if (portCSBackground) { + portCSBackground.postMessage(msg); + } else { + console.log("failed to send message to content script: port disconnected", msg); + } +} diff --git a/object_database/web/devtools/content-script.js b/object_database/web/devtools/content-script.js index a1c02e588..304adb0e8 100644 --- a/object_database/web/devtools/content-script.js +++ b/object_database/web/devtools/content-script.js @@ -13,12 +13,12 @@ console.log("loading content script"); -var portFromCS = chrome.runtime.connect({name:"port-from-cs"}); +var portCSBackground = chrome.runtime.connect({name:"port-cs-background"}); // at the moment nothing much is done with messages going // to the content-script port -portFromCS.onMessage.addListener(function(msg) { - // console.log("received message from background"); +portCSBackground.onMessage.addListener(function(msg) { + console.log("received message from background: ", msg); }); window.addEventListener("message", (event) => { @@ -28,7 +28,7 @@ window.addEventListener("message", (event) => { // filter the message further to make sure it's for devtools if(event.data.type == "cells_devtools"){ // reroute the message to the background script - portFromCS.postMessage({data: event.data}); + portCSBackground.postMessage({data: event.data}); } } }, false); diff --git a/object_database/web/devtools/devtools_init.js b/object_database/web/devtools/devtools_init.js index 05cbb10d1..b54b0a15c 100644 --- a/object_database/web/devtools/devtools_init.js +++ b/object_database/web/devtools/devtools_init.js @@ -9,8 +9,8 @@ chrome.devtools.panels.create( // create a connection/port which will handle all communication // between the panel and the background script - const portFromPanel = chrome.runtime.connect({name: "port-from-panel"}); - portFromPanel.onMessage.addListener(function(msg) { + const portPanelBackground = chrome.runtime.connect({name: "port-panel-background"}); + portPanelBackground.onMessage.addListener(function(msg) { if (_window){ // handleMessageFromBackground() is defined in cell_panel.js _window.handleMessageFromBackground(msg); @@ -40,13 +40,20 @@ chrome.devtools.panels.create( _window.handleMessageFromBackground(msg); msg = data.shift(); } + // let the content-script ie target window, know + // that the panel has loaded + portPanelBackground.postMessage({ + action: "notifyCS", + data: "panel is open" + }); // If we ever need to send messages back via the port // we can do that as below _window.sendMessageToBackground = function(msg) { - portFromPanel.postMessage(msg); + portPanelBackground.postMessage(msg); } }); - panel.onHidden.addListener(function() {console.log("panel is being hidden")}); console.log(panel); + panel.onHidden.addListener(function() { + console.log("panel is being hidden")}); } ); From 15da70b74ae609148501a13058107264b5fde769 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 19 Jun 2023 13:22:21 +0200 Subject: [PATCH 55/57] refactoring inspected window handling CS message handling moving forward the background will not use the direct inspected window eval() - which is a) awkward and b) can have security issues. All communication from the cells panel routes via the background, to the content script which then handles messages as needed (either directly executing utils or sending messages via the window message api) --- .../web/content/src/CellHandler.js | 22 +---- .../web/devtools/content-script.js | 6 +- object_database/web/devtools/js/cell_panel.js | 81 +++++++++---------- .../web/devtools/js/inspect_window_utils.js | 58 +++++++++++++ object_database/web/devtools/manifest.json | 3 +- 5 files changed, 104 insertions(+), 66 deletions(-) create mode 100644 object_database/web/devtools/js/inspect_window_utils.js diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index af8bf1e66..14c121c41 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -514,27 +514,6 @@ class CellHandler { * TODO: potentially this should pass via the content-scripts */ setupDevtools(){ - const overlayId = "cells-devtools-overlay"; - window.addDevtoolsHighlight = (id) => { - const cell = document.querySelector(`[data-cell-id="${id}"]`); - const rect = cell.getBoundingClientRect(); - const overlay = document.createElement("div"); - overlay.style.position = "absolute"; - overlay.style.backgroundColor = "#cec848"; - overlay.style.opacity = "0.5"; - overlay.style.left = rect.left +"px"; - overlay.style.top = rect.top + "px"; - overlay.style.height = rect.height + "px"; - overlay.style.width = rect.width + "px"; - overlay.setAttribute("id", overlayId); - document.body.append(overlay); - } - - window.removeDevtoolsHighlight = () => { - const overlays = document.querySelectorAll(`#${overlayId}`); - overlays.forEach((el) => el.remove()); - } - window.sendCellSource = (id) => { this.sendMessageToCells({ event: "devtoolsRequest", @@ -543,6 +522,7 @@ class CellHandler { }); } } + sendMessageToDevtools(msg){ // TODO perhaps this should be run by a worker msg.type = "cells_devtools"; diff --git a/object_database/web/devtools/content-script.js b/object_database/web/devtools/content-script.js index 304adb0e8..1c2241c19 100644 --- a/object_database/web/devtools/content-script.js +++ b/object_database/web/devtools/content-script.js @@ -11,14 +11,16 @@ * originating messaged to the devtools background. */ - console.log("loading content script"); var portCSBackground = chrome.runtime.connect({name:"port-cs-background"}); // at the moment nothing much is done with messages going // to the content-script port portCSBackground.onMessage.addListener(function(msg) { - console.log("received message from background: ", msg); + //console.log("received message from background: ", msg); + // this handlder is defined in ./js/inspect_window_utils.js + // and is made globally available in the manifest.js file + handleBackgroundMessage(msg); }); window.addEventListener("message", (event) => { diff --git a/object_database/web/devtools/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js index ee7dd9926..5f692b88b 100644 --- a/object_database/web/devtools/js/cell_panel.js +++ b/object_database/web/devtools/js/cell_panel.js @@ -4,7 +4,9 @@ let state = null; let cellsJSONCache = null; -// setup message handling from background +/** + * Messages and related + */ function handleMessageFromBackground(msg){ // console.log("handling background message"); // console.log(msg); @@ -34,6 +36,9 @@ function handleMessageFromBackground(msg){ window.handleMessageFromBackground = handleMessageFromBackground; +/** + * Display lifecycle. + */ const initialLoadDisplay = () => { const main = document.getElementById("main"); main.textContent = "Initializing: no cells loaded"; @@ -44,36 +49,12 @@ const reconnectingDisplay = () => { main.textContent = "Reconnecting: no cells loaded"; } -const updateInfoPanel = (node) => { - const infoPanel = document.getElementById("cell-info"); - const id = node.getAttribute("data-original-id"); - // we need to retrieve the source code for the node - chrome.devtools.inspectedWindow.eval( - `window.sendCellSource(${id})` - ); - const name = node.name; - let info = `${name}\ncell-id: ${id}`; - const tree = document.querySelector("tree-graph"); - const parentSubtree = tree.findParentSubTree(id, tree.data); - if (parentSubtree.name.match("Subscribed")) { - info = `${info}\nsubscribed to cell-id: ${parentSubtree.id}`; - } - - console.log("Clicked on node, sending message to background") - window.sendMessageToBackground("I need more data here"); - /* - const nodeTree = parentSubtree.children.filter((n) => { - return n.id = node.id; - })[0] - let childIds = ""; - nodeTree.children.forEach((c) => { - childIds = `${childIds}, ${c.id}`; - }); - info = `${info}\nchild node ids: ${childIds}`; - */ - infoPanel.innerText = info; +const clearDisplay = () => { + document.getElementById("main").replaceChildren(); + document.getElementById("cell-info").replaceChildren(); } +// display the tree const cellsTreeDisplay = (cells) => { clearDisplay(); // init and run @@ -90,15 +71,18 @@ const cellsTreeDisplay = (cells) => { tree.onNodeMouseover = (event) => { // highlight the corresponding element in the target window const id = event.target.getAttribute("data-original-id"); - chrome.devtools.inspectedWindow.eval( - `window.addDevtoolsHighlight(${id})` - ); + window.sendMessageToBackground({ + action: "notifyCS", + event: "mouseover", + nodeId: id + }) } tree.onNodeMouseleave = (event) => { // un-highlight the corresponding element in the target window - chrome.devtools.inspectedWindow.eval( - `window.removeDevtoolsHighlight()` - ); + window.sendMessageToBackground({ + action: "notifyCS", + event: "mouseleave", + }) } tree.onNodeClick = (event) => { @@ -116,12 +100,25 @@ const cellsTreeDisplay = (cells) => { tree.setup(cells); } +// info panel display +const updateInfoPanel = (node) => { + const infoPanel = document.getElementById("cell-info"); + const id = node.getAttribute("data-original-id"); + // we need to retrieve the source code for the node + window.sendMessageToBackground({ + action: "notifyCS", + event: "click", + nodeId: id, + request: "source" + }) + const name = node.name; + let info = `${name}\ncell-id: ${id}`; + const tree = document.querySelector("tree-graph"); + const parentSubtree = tree.findParentSubTree(id, tree.data); + if (parentSubtree.name.match("Subscribed")) { + info = `${info}\nsubscribed to cell-id: ${parentSubtree.id}`; + } -/** - * I clear the views when the application - * views when the application state changes - **/ -const clearDisplay = () => { - document.getElementById("main").replaceChildren(); - document.getElementById("cell-info").replaceChildren(); + infoPanel.innerText = info; } + diff --git a/object_database/web/devtools/js/inspect_window_utils.js b/object_database/web/devtools/js/inspect_window_utils.js new file mode 100644 index 000000000..ee00bf793 --- /dev/null +++ b/object_database/web/devtools/js/inspect_window_utils.js @@ -0,0 +1,58 @@ +/** + * Utilities and helpers for the inspected + * (target) window. These are callbacks for + * various messages handled by the CS <-> background + * port listeners found in the content_script.js file. + */ + + +const handleBackgroundMessage = (msg) => { + switch (msg.event) { + case "mouseover": + onMouseOver(msg.nodeId); + break; + + case "mouseleave": + onMouseLeave(); + break; + + case "click": + onClick(msg.nodeId); + } +} + +// Helpers +const overlayId = "cells-devtools-overlay"; + +const highlightCellNode = (id) => { + const cell = document.querySelector(`[data-cell-id="${id}"]`); + const rect = cell.getBoundingClientRect(); + const overlay = document.createElement("div"); + overlay.style.position = "absolute"; + overlay.style.backgroundColor = "#cec848"; + overlay.style.opacity = "0.5"; + overlay.style.left = rect.left + "px"; + overlay.style.top = rect.top + "px"; + overlay.style.height = rect.height + "px"; + overlay.style.width = rect.width + "px"; + overlay.setAttribute("id", overlayId); + document.body.append(overlay); +} + +const clearCellNodeHighlight = () => { + const overlays = document.querySelectorAll(`#${overlayId}`); + overlays.forEach((el) => el.remove()); +} + +// event listeners +const onMouseOver = (id) => { + highlightCellNode(id); +} + +const onMouseLeave = () => { + clearCellNodeHighlight(); +} + +const onClick = (id) => { + console.log("clicked on cell id: ", id); +} diff --git a/object_database/web/devtools/manifest.json b/object_database/web/devtools/manifest.json index 27b9c1883..8b4d37df9 100644 --- a/object_database/web/devtools/manifest.json +++ b/object_database/web/devtools/manifest.json @@ -10,7 +10,8 @@ "*://*/*" ], "js": [ - "content-script.js" + "content-script.js", + "js/inspect_window_utils.js" ] } ], From 3f18df39a5c0717e681c1d76b25482e60d407940 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 19 Jun 2023 13:40:25 +0200 Subject: [PATCH 56/57] setting up cell source code request via the window message api --- .../web/content/src/CellHandler.js | 20 ++++++------ .../web/devtools/js/inspect_window_utils.js | 32 ++++++++++++------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 14c121c41..8a4c01dd7 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -510,17 +510,18 @@ class CellHandler { } /** - * Setup utilities for devtools to call using chrome.devtools.inspectedWindow.eval() - * TODO: potentially this should pass via the content-scripts + * Devtools */ setupDevtools(){ - window.sendCellSource = (id) => { - this.sendMessageToCells({ - event: "devtoolsRequest", - request: "source", - cellId: id - }); - } + window.addEventListener("message", (event) => { + // filter on the target windows url + if (event.origin === window.location.origin) { + // listent to only devtools content-script messages + if (event.data.type == "cells_devtools_CS") { + console.log("receiving message from CS: ", event.data); + } + } + }) } sendMessageToDevtools(msg){ @@ -552,6 +553,7 @@ class CellHandler { if (child instanceof Array){ child.forEach((subchild) => { const subTree = buildTree(subchild); + window.postMessage(msg); if (subTree){ children.push(buildTree(subchild)); } diff --git a/object_database/web/devtools/js/inspect_window_utils.js b/object_database/web/devtools/js/inspect_window_utils.js index ee00bf793..ba696cd4f 100644 --- a/object_database/web/devtools/js/inspect_window_utils.js +++ b/object_database/web/devtools/js/inspect_window_utils.js @@ -26,17 +26,19 @@ const overlayId = "cells-devtools-overlay"; const highlightCellNode = (id) => { const cell = document.querySelector(`[data-cell-id="${id}"]`); - const rect = cell.getBoundingClientRect(); - const overlay = document.createElement("div"); - overlay.style.position = "absolute"; - overlay.style.backgroundColor = "#cec848"; - overlay.style.opacity = "0.5"; - overlay.style.left = rect.left + "px"; - overlay.style.top = rect.top + "px"; - overlay.style.height = rect.height + "px"; - overlay.style.width = rect.width + "px"; - overlay.setAttribute("id", overlayId); - document.body.append(overlay); + if (cell) { + const rect = cell.getBoundingClientRect(); + const overlay = document.createElement("div"); + overlay.style.position = "absolute"; + overlay.style.backgroundColor = "#cec848"; + overlay.style.opacity = "0.5"; + overlay.style.left = rect.left + "px"; + overlay.style.top = rect.top + "px"; + overlay.style.height = rect.height + "px"; + overlay.style.width = rect.width + "px"; + overlay.setAttribute("id", overlayId); + document.body.append(overlay); + } } const clearCellNodeHighlight = () => { @@ -54,5 +56,11 @@ const onMouseLeave = () => { } const onClick = (id) => { - console.log("clicked on cell id: ", id); + // get the source code for the cell and send over the devtools + const msg = { + type: "cells_devtools_CS", + request: "source", + nodeId: id + }; + window.postMessage(msg); } From 1dff900711ee255ca3a303a5820d800a8876ddd5 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Mon, 19 Jun 2023 13:43:12 +0200 Subject: [PATCH 57/57] typo correction --- object_database/web/content/src/CellHandler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/object_database/web/content/src/CellHandler.js b/object_database/web/content/src/CellHandler.js index 8a4c01dd7..a8a40a5c5 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -553,7 +553,6 @@ class CellHandler { if (child instanceof Array){ child.forEach((subchild) => { const subTree = buildTree(subchild); - window.postMessage(msg); if (subTree){ children.push(buildTree(subchild)); }