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..a1f8b7cd5 100644 --- a/object_database/web/content/src/CellHandler.js +++ b/object_database/web/content/src/CellHandler.js @@ -58,6 +58,10 @@ class CellHandler { // the server-assigned session this.sessionId = null; + + // devtools related messages + this.sendMessageToDevtools = this.sendMessageToDevtools.bind(this); + this.updateDevtools = this.updateDevtools.bind(this); } tearDownAllLiveCells() { @@ -127,6 +131,9 @@ class CellHandler { ]) ]) ); + this.sendMessageToDevtools({ + status: "initial load" + }); } afterConnected() { @@ -158,6 +165,9 @@ class CellHandler { ["Reconnecting in " + waitSeconds + " seconds"]) ]) ); + this.sendMessageToDevtools({ + status: "reconnecting" + }); } /** @@ -167,7 +177,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 +375,7 @@ class CellHandler { + totalUpdateCount + " nodes created/updated." ) } + this.updateDevtools(); } } @@ -495,6 +506,49 @@ 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 buildTree = (cell) => { + return { + name: cell.constructor.name, + identity: 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) => { + children.push(buildTree(subchild)); + }) + } + 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..b97fc84c0 --- /dev/null +++ b/object_database/web/devtools/background.js @@ -0,0 +1,56 @@ +/* + * 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(msg); + 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/cells_panel.css b/object_database/web/devtools/cells_panel.css new file mode 100644 index 000000000..eff198b16 --- /dev/null +++ b/object_database/web/devtools/cells_panel.css @@ -0,0 +1,46 @@ +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%; +} + +.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..a89140912 --- /dev/null +++ b/object_database/web/devtools/cells_panel.html @@ -0,0 +1,14 @@ + + +
+ +