From 4cd4df908709027cd71c6001a07d0bdf908abd49 Mon Sep 17 00:00:00 2001 From: dkrasner Date: Sat, 1 Oct 2022 19:14:23 +0200 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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; } );