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/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/object_database/web/cells/cells.py b/object_database/web/cells/cells.py
index b555c3c45..c0a597fbf 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,22 @@ def onMessage(self, message):
if cell is not None:
cell.mostRecentFocusId = self.focusEventId
+ if message.get("event") == "devtoolsRequest":
+ cellId = str(message.get("cellId"))
+ 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("response %s" % data)
+ return data
+ """
+
def _executeCallback(self, callback, withLogging=True):
context = DependencyContext(self, readOnly=False)
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 7664f035c..a8a40a5c5 100644
--- a/object_database/web/content/src/CellHandler.js
+++ b/object_database/web/content/src/CellHandler.js
@@ -58,6 +58,11 @@ class CellHandler {
// the server-assigned session
this.sessionId = null;
+
+ // devtools related messages
+ this.sendMessageToDevtools = this.sendMessageToDevtools.bind(this);
+ this.updateDevtools = this.updateDevtools.bind(this);
+ this.setupDevtools = this.setupDevtools.bind(this);
}
tearDownAllLiveCells() {
@@ -127,6 +132,10 @@ class CellHandler {
])
])
);
+ this.setupDevtools();
+ this.sendMessageToDevtools({
+ status: "initial load"
+ });
}
afterConnected() {
@@ -158,6 +167,9 @@ class CellHandler {
["Reconnecting in " + waitSeconds + " seconds"])
])
);
+ this.sendMessageToDevtools({
+ status: "reconnecting"
+ });
}
/**
@@ -167,7 +179,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
@@ -365,6 +377,7 @@ class CellHandler {
+ totalUpdateCount + " nodes created/updated."
)
}
+ this.updateDevtools();
}
}
@@ -495,6 +508,72 @@ class CellHandler {
this.socket.sendString(JSON.stringify(message));
}
}
+
+ /**
+ * Devtools
+ */
+ setupDevtools(){
+ 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){
+ // TODO perhaps this should be run by a worker
+ msg.type = "cells_devtools";
+ window.postMessage(msg);
+ }
+
+ /**
+ * Send updated cells data to devtools
+ **/
+ updateDevtools(){
+ const buildTree = (cell) => {
+ 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
+ // 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) => {
+ const subTree = buildTree(subchild);
+ if (subTree){
+ children.push(buildTree(subchild));
+ }
+ })
+ }
+ const subTree = buildTree(child);
+ if (subTree) {
+ children.push(buildTree(child));
+ }
+ }
+ })
+ }
+ return children;
+ }
+ const page_root = this.activeCells['page_root'];
+ const tree = buildTree(page_root);
+ this.sendMessageToDevtools({
+ status: "loaded",
+ cells: tree
+ });
+ }
}
export {CellHandler, CellHandler as default};
diff --git a/object_database/web/devtools/DevtoolsExtensions.png b/object_database/web/devtools/DevtoolsExtensions.png
new file mode 100644
index 000000000..2fc5d466f
Binary files /dev/null and b/object_database/web/devtools/DevtoolsExtensions.png differ
diff --git a/object_database/web/devtools/README.md b/object_database/web/devtools/README.md
new file mode 100644
index 000000000..eadb20b2b
--- /dev/null
+++ b/object_database/web/devtools/README.md
@@ -0,0 +1,71 @@
+## Chrome Devtools Extensions ##
+
+The code here is devoted to building devtool extensions and related development.
+
+**NOTE: as of writing this is tested only in Chrome 106.X.**
+
+### Installation ###
+
+Open the extension manager in chrome. Then press "Load Temporary Add-on," select the [manifest.json](./manifest.json) file and press "open." When loading one of our example applications and opening devtools you should see a "Cells" panel.
+
+### 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 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).
+
+The rest of the story consists of understanding all the necessary components and how these communicate. This is summarized in the image below:
+
+
+
+With the above "knowledge" you should already be able to guess that everything in the "Devtools" box will log to the devtool's devtool console, while everything in the "Document" box will log to the document's devtools console.
+
+Lets go through these one at a time:
+
+[devtools_init.html](./devtools_init.js) and [devtools_init.js](./devtools_init.js)
+-------------------------------------------------------------------
+
+ 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 `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`.
+
+Note also, the var `_window` is used here to deal with the possibility that at times the message inspector panel is closed (for example you are looking at console or something else) but there is still message passing going on and data needs to be stored. In this case the panel collects data which will be subsequently processed when the panel opens. This is the core of what is defined in the `devtools_init.js` file.
+
+[panels](./js)
+------------------
+
+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 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.
+
+
+[background.js](./background.js)
+--------------------------------
+
+[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 `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 `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.
+
+Now it's pretty clear what content scripts should do:
+* set up a port to communication with background,
+* listen to messages coming via the `window.postMessage` interface and forward whatever is needed to background.
+
+The messages that content script here is waiting on are sent in `System.sendMessage()` in our application.
+
+And that's more or less it.
diff --git a/object_database/web/devtools/background.js b/object_database/web/devtools/background.js
new file mode 100644
index 000000000..7e56bad14
--- /dev/null
+++ b/object_database/web/devtools/background.js
@@ -0,0 +1,63 @@
+/*
+ * 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 portCSBackground;
+var portPanelBackground;
+
+function connected(port) {
+ // handle all communication to and from the panel
+ if (port.name === "port-panel-background"){
+ portPanelBackground = port;
+ // at the moment we don't do anything with messages coming
+ // from the panels
+ 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-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
+ // debugger console
+ notifyDevtoolsPanel(msg.data);
+ });
+ }
+ // notify if the port has disconnected
+ port.onDisconnect.addListener(function(port) {
+ if (port.name === "port-panel-background" || port.name === "port-cs-background"){
+ console.log(`${port.name} has disconnected`);
+ };
+ });
+}
+
+chrome.runtime.onConnect.addListener(connected);
+
+function notifyDevtoolsPanel(msg){
+ if (portPanelBackground){
+ portPanelBackground.postMessage(msg);
+ } else {
+ console.log(msg);
+ console.log("failed to send message to devtools panel: port disconnected");
+ }
+}
+
+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/cells_panel.css b/object_database/web/devtools/cells_panel.css
new file mode 100644
index 000000000..369acfdb6
--- /dev/null
+++ b/object_database/web/devtools/cells_panel.css
@@ -0,0 +1,49 @@
+html {
+ height: 100% !important;
+}
+
+body {
+ height: 100% !important;
+ display: flex;
+ flex-direction: row;
+ margin: 0px;
+}
+
+div#main {
+ display: flex;
+ justify-content: center;
+ min-width: 70%;
+ border-right: solid 2px;
+ align-items: center;
+}
+
+div#cell-info {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-width: 30%;
+ text-align: center;
+ font-family: Roboto;
+ font-size: 20px;
+}
+
+.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
new file mode 100644
index 000000000..b53cefe6b
--- /dev/null
+++ b/object_database/web/devtools/cells_panel.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Cells
+
+
+
+
+
+
+
+
some cells data here
+
+
+
diff --git a/object_database/web/devtools/content-script.js b/object_database/web/devtools/content-script.js
new file mode 100644
index 000000000..1c2241c19
--- /dev/null
+++ b/object_database/web/devtools/content-script.js
@@ -0,0 +1,36 @@
+/*
+ * I am the content 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.
+ */
+
+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);
+ // 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) => {
+ // filter on the target windows url
+ // console.log("message in target window")
+ if(event.origin === window.location.origin){
+ // filter the message further to make sure it's for devtools
+ if(event.data.type == "cells_devtools"){
+ // reroute the message to the background script
+ portCSBackground.postMessage({data: event.data});
+ }
+ }
+}, false);
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..b54b0a15c
--- /dev/null
+++ b/object_database/web/devtools/devtools_init.js
@@ -0,0 +1,59 @@
+// Create a new panel
+chrome.devtools.panels.create(
+ "Cells",
+ null,
+ "cells_panel.html",
+ function(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 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);
+ } 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:`)
+ // console.log(data);
+ }
+ });
+
+ // 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;
+ let msg = data.shift();
+ // if any data was logged while the panel was not available
+ // send it along now
+ while (msg){
+ // console.log("handling logged messages");
+ _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) {
+ portPanelBackground.postMessage(msg);
+ }
+ });
+
+ panel.onHidden.addListener(function() {
+ console.log("panel is being hidden")});
+ }
+);
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/js/cell_panel.js b/object_database/web/devtools/js/cell_panel.js
new file mode 100644
index 000000000..5f692b88b
--- /dev/null
+++ b/object_database/web/devtools/js/cell_panel.js
@@ -0,0 +1,124 @@
+// import {CellsTree} from './tree.js';
+
+// GLOBALS (TODO: should be handled better)
+let state = null;
+let cellsJSONCache = null;
+
+/**
+ * Messages and related
+ */
+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":
+ // 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);
+ }
+ }
+}
+
+window.handleMessageFromBackground = handleMessageFromBackground;
+
+
+/**
+ * Display lifecycle.
+ */
+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 clearDisplay = () => {
+ document.getElementById("main").replaceChildren();
+ document.getElementById("cell-info").replaceChildren();
+}
+
+// display the tree
+const cellsTreeDisplay = (cells) => {
+ clearDisplay();
+ // init and run
+ // NOTE: the tree class itself attaches the
+ // svg element to #main
+ // const cTree = new CellsTree(cells);
+ // cTree.setupTree();
+ 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) => {
+ // highlight the corresponding element in the target window
+ const id = event.target.getAttribute("data-original-id");
+ window.sendMessageToBackground({
+ action: "notifyCS",
+ event: "mouseover",
+ nodeId: id
+ })
+ }
+ tree.onNodeMouseleave = (event) => {
+ // un-highlight the corresponding element in the target window
+ window.sendMessageToBackground({
+ action: "notifyCS",
+ event: "mouseleave",
+ })
+ }
+
+ 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);
+}
+
+// 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}`;
+ }
+
+ 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..ba696cd4f
--- /dev/null
+++ b/object_database/web/devtools/js/inspect_window_utils.js
@@ -0,0 +1,66 @@
+/**
+ * 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}"]`);
+ 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 = () => {
+ const overlays = document.querySelectorAll(`#${overlayId}`);
+ overlays.forEach((el) => el.remove());
+}
+
+// event listeners
+const onMouseOver = (id) => {
+ highlightCellNode(id);
+}
+
+const onMouseLeave = () => {
+ clearCellNodeHighlight();
+}
+
+const onClick = (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);
+}
diff --git a/object_database/web/devtools/manifest.json b/object_database/web/devtools/manifest.json
new file mode 100644
index 000000000..8b4d37df9
--- /dev/null
+++ b/object_database/web/devtools/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "cells devtools",
+ "version": "1.0",
+ "manifest_version": 2,
+ "devtools_page": "devtools.html",
+ "content_security_policy": "script-src 'self' https://d3js.org; object-src 'self'",
+ "content_scripts": [
+ {
+ "matches": [
+ "*://*/*"
+ ],
+ "js": [
+ "content-script.js",
+ "js/inspect_window_utils.js"
+ ]
+ }
+ ],
+ "background": {
+ "scripts": [
+ "background.js"
+ ]
+ }
+}
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
new file mode 100644
index 000000000..d30c7419f
--- /dev/null
+++ b/object_database/web/devtools/tree/Tree.js
@@ -0,0 +1,404 @@
+/**
+ * Tree Graph Web component
+ **/
+
+
+// Simple grid-based sheet component
+const templateString = `
+
+
+`;
+
+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));
+
+ 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);
+ 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);
+ }
+
+ connectedCallback(){
+ if(this.isConnected){
+ // add event listeners
+ window.addEventListener("resize", this.onWindowResize);
+ document.addEventListener("keyup", this.onKeyUp);
+ this.setAttribute("display-depth", 3);
+ }
+ }
+
+ disconnectedCallback(){
+ document.removeEventListener("key", this.onKeyUp);
+ }
+
+ customizeNode(node){
+ //Noop, to be used by consumers
+ }
+
+ setup(data, cache=true){
+ this.clear();
+ if (cache){
+ // cache the data; TODO: think through this
+ this.data = data;
+ }
+ const wrapper = this.shadowRoot.querySelector("#wrapper");
+ // wrapper.addEventListener("dblclick", this.onNodeDblclick);
+ const nodeDepth = document.createElement("div");
+ nodeDepth.classList.add("depth");
+ nodeDepth.setAttribute("id", "depth-0");
+ wrapper.append(nodeDepth);
+ const nodeWrapper = document.createElement("div");
+ nodeWrapper.classList.add("child-wrapper");
+ nodeDepth.append(nodeWrapper);
+ this.setupNode(data, nodeWrapper, 1, true); // this is a root node
+ // setup the node paths
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', "svg");
+ svg.setAttribute("width", wrapper.getBoundingClientRect().width);
+ svg.setAttribute("height", "100%");
+ svg.style.position = "absolute";
+ svg.style.left = 0;
+ svg.style.top = 0;
+ wrapper.append(svg);
+ this.setupPaths(svg, data);
+ // fade the wrapper
+ wrapper.classList.remove("animation-fade-out");
+ wrapper.classList.add("animation-fade-in");
+ }
+
+ setupNode(nodeData, wrapperDiv, depth, root=false){
+ if(nodeData){
+ const node = document.createElement("tree-node");
+ node.name = nodeData.name;
+ // 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);
+ // 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
+ // 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);
+ }
+ }
+ this.customizeNode(node);
+ wrapperDiv.append(node);
+ // 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
+ 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");
+ wrapper.append(depthDiv);
+ }
+ const childWrapper = document.createElement("div");
+ childWrapper.classList.add("child-wrapper");
+ depthDiv.append(childWrapper);
+ nodeData.children.forEach((childData) => {
+ this.setupNode(childData, childWrapper, depth + 1);
+
+ })
+ }
+ return node;
+ }
+ }
+
+ setupPaths(svg, nodeData){
+ if (nodeData) {
+ this.attainedDepth += 1;
+ const parent = this.shadowRoot.querySelector(`#node-${nodeData.id}`);
+ nodeData.children.forEach((childData) => {
+ const child = this.shadowRoot.querySelector(`#node-${childData.id}`);
+ if (parent && child) {
+ this.addSVGPath(svg, parent, child);
+ this.setupPaths(svg, 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
+ * NODE: svg-type elemnents need to be created using the
+ * SVG name space, ie document.createElementNS...
+ * @param {svg-element} svg
+ * @param {element} startNode
+ * @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;
+ const startX = startRect.left + (startRect.width / 2);
+ const endRect = endNode.getBoundingClientRect();
+ const endY = endRect.top;
+ const endX = endRect.left + (endRect.width / 2);
+
+ let d; // this is the path data
+ 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;
+ 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;
+ */
+ console.log("new controls")
+
+ d = `M ${startX} ${startY} Q ${controlX} ${controlY}, ${midX} ${midY} T ${endX} ${endY}`;
+ }
+ path.setAttribute("d", d);
+ svg.append(path);
+ }
+
+ /**
+ * On a window resize I first clear the tree and then redraw it
+ **/
+ onWindowResize(event){
+ this.setup(this.data);
+ }
+
+ onNodeDblclick(event){
+ // if clicking on the root node, reset back to cached this.data tree
+ 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
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ onNodeMouseover(event) {
+ // no-op
+ }
+
+ 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
+ **/
+ 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;
+ }
+
+ /**
+ * I recursively walk the tree to find the corresponding
+ * parent node, and when I do I return its subtree
+ **/
+ findParentSubTree(id, node){
+ // if already at the top of the tree return it
+ if (id == this.data.id) {
+ return this.data;
+ }
+ let subTree;
+ const isParent = node.children.some((child) => child.id == id)
+ if (isParent) {
+ subTree = node;
+ } else {
+ node.children.forEach((childNode) => {
+ const out = this.findParentSubTree(id, childNode);
+ if (out) {
+ subTree = out;
+ }
+ })
+ }
+ return subTree;
+ }
+ clear(){
+ const wrapper = this.shadowRoot.querySelector("#wrapper");
+ wrapper.classList.remove("animation-fade-in");
+ 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);
+
+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..941c466c1
--- /dev/null
+++ b/object_database/web/devtools/tree/TreeNode.js
@@ -0,0 +1,53 @@
+/**
+ * Tree Node Web component
+ **/
+
+
+// Simple grid-based sheet component
+const templateString = `
+
+