Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions object_database/web/content/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1096,3 +1096,8 @@ pre {
overflow: auto;
pointer-events: auto;
}

// devtools helpers
.devtools-inspect{
background-color: lightblue!important;
}
56 changes: 55 additions & 1 deletion object_database/web/content/src/CellHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -127,6 +131,9 @@ class CellHandler {
])
])
);
this.sendMessageToDevtools({
status: "initial load"
});
}

afterConnected() {
Expand Down Expand Up @@ -158,6 +165,9 @@ class CellHandler {
["Reconnecting in " + waitSeconds + " seconds"])
])
);
this.sendMessageToDevtools({
status: "reconnecting"
});
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -365,6 +375,7 @@ class CellHandler {
+ totalUpdateCount + " nodes created/updated."
)
}
this.updateDevtools();
}
}

Expand Down Expand Up @@ -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};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions object_database/web/devtools/README.md
Original file line number Diff line number Diff line change
@@ -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:

![Devtools Extension Setup](DevtoolsExtensions.png)

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.
56 changes: 56 additions & 0 deletions object_database/web/devtools/background.js
Original file line number Diff line number Diff line change
@@ -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!"});
// });
46 changes: 46 additions & 0 deletions object_database/web/devtools/cells_panel.css
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions object_database/web/devtools/cells_panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Cells</title>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="cells_panel.css">
</head>
<body>
<div id="main"></div>
<div id="cell-info"> some cells data here</div>
</body>
<script type="module" src="js/cell_panel.js"></script>
</html>
34 changes: 34 additions & 0 deletions object_database/web/devtools/content-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 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("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){
// 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);
6 changes: 6 additions & 0 deletions object_database/web/devtools/devtools.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<body>
<script src="devtools_init.js"></script>
</body>
</html>
52 changes: 52 additions & 0 deletions object_database/web/devtools/devtools_init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 portFromPanel = chrome.runtime.connect({name: "port-from-panel"});
portFromPanel.onMessage.addListener(function(msg) {
if (_window){
// handleMessageFromBackground() is defined in 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();
}
// 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);
}
);
Loading