Custom components
Marginalia has almost no hand-written frontend code, but three components draw with a JavaScript library that Vaadin
doesn't ship: a tree, a chart and a graph. This page documents them, how they get their library, and how to add
another one. Plugins can use all three; a plugin can't bring a library of its own (see
All three work the same way:
-
The Java class is a
Divannotated with@NpmPackage(the library, downloaded by the Vaadin frontend build) and@JsModule(the connector insrc/main/frontend). -
The connector is a small ES module that imports the library, exports a function that draws into an element and registers it as
window.<name>. -
The Java method sends the data as JSON to
JavascriptLibrary.<name>(element, json), which runswindow.<name>(element, JSON.parse(json))with the component's element. All scripts the application runs are static methods ofJavascriptLibrary(User interface); a component never callsexecuteJsitself. -
Drawing again with new data replaces what was drawn.
The script is queued by Vaadin until the element is attached, so a component may get its data before it is shown (e.g. in the constructor of a dialog).
TreantTree
ui/components/TreantTree.java, frontend/treant-connector.js. npm packages treant-js and raphael, Treant's CSS
is imported with @CssImport.
setTreeConfig(json) takes a Treant configuration with one change: instead of the nested nodeStructure, nodes is a
flat list, and the connector builds the tree:
{
"chart": {"connectors": {"type": "step"}, "node": {"collapsable": false}},
"nodes": [
{"id": "1", "innerHTML": "<b>root</b>", "HTMLclass": "tree-node", "HTMLid": "1", "data": {"nodeId": "1"}},
{"id": "2", "parentId": "1", "innerHTML": "child", "HTMLclass": "tree-node"}
]
}
Events, as addNodeClickListener / addNodeGotoListener:
-
NodeClickEvent- a node was clicked:getNodeId()(thedata-node-idof the node or of an element in it, else the node'sHTMLidwithout anode-prefix),getNodeName()andgetNodeTitle()(text of.node-nameand.node-titleinside it). -
NodeGotoEvent- an element withdata-goto-idinside a node was clicked (the node isn't clicked then):getNodeId()is that attribute.
The look is .Treant and .Treant .node in shared-styles.css, written with !important because Treant sets its own
inline styles. A tree is only a tree: a graph with cycles or nodes with several parents has to be cut into trees
and repeated nodes, which is why the graph below exists.
ChartJs
ui/widgets/ChartJs.java, frontend/chart-connector.js. npm package chart.js.
setChartConfig(json) takes a Chart.js configuration (type, data, options) and draws it responsively into the
element (320px high, chart-js style in shared-styles.css; setHeight changes it). The connector takes the text and
grid colors from the Lumo variables of the page, so charts follow the theme. Examples: the doughnut and bar charts of
TokenUsageDialog and the charts of AgentContextView.
GraphView
ui/widgets/GraphView.java, frontend/graph-connector.js. npm package cytoscape. It draws what a tree can't:
nodes that are connected in a cycle or to several others.
setGraph(json):
{
"nodes": [
{"id": "a", "label": "Alice\nthe baker", "color": "#ffcc00", "textColor": "#000000"},
{"id": "b", "label": "Bob", "color": "#102030", "textColor": "#ffffff", "image": "data:image/png;base64,..."}
],
"edges": [
{"id": "e1", "source": "a", "target": "b", "label": "employs\nsince spring", "bothWays": false}
],
"centerId": "a"
}
The nodes are laid out in rings around the center (the concentric layout): the center in the middle, its
neighbours on the first ring, theirs on the second and so on, with the distance counted in edges, ignoring direction;
nodes the center can't reach are on the outermost ring. A double click (or double tap) on a node arranges the graph
around that node, animated, in the browser - there is no round trip to the server. The user can drag nodes and pan
and zoom the view. The component fills its parent (graph-view style, at least 300px high), so give the parent a
size, e.g. a VerticalLayout with setSizeFull() and setFlexGrow(1, graph) in a dialog; it follows later size
changes (a resizable dialog) with a ResizeObserver.
Theme colors of the lines and labels are read from the Lumo variables when the graph is drawn and passed through a canvas, because the browser may compute a color to a form that Cytoscape can't parse. A theme change after that isn't followed until the graph is drawn again.
Adding a component for another library
-
Add the Java class (a
Div) with@NpmPackage(value = "<package>", version = "^x.y.z")and@JsModule("./<name>-connector.js"). The Maven build downloads the package with the rest of the frontend; the first build after adding one is slow. -
Write
src/main/frontend/<name>-connector.js: import the library, exportrender<Name>(element, config)that destroys what it drew intoelementbefore, and setwindow.render<Name> = render<Name>. Take colors from the Lumo variables rather than hard-coding them. -
Add the static method to
JavascriptLibrary(element.executeJs("if (window.render<Name>) { window.render<Name>($0, JSON.parse($1)); }", element, json)). -
If the element needs a size or a positioning context, add the rule to
shared-styles.cssand its name toCssStyles(both with the same name). -
Mark the class
@Extendableand give each public method@PublicAPI, runmvn test -Dtest=PublicAPISignatureTestand paste what it prints (Extendable hooks). -
Note it here, in Project structure (components, frontend files) and in the changelog.
Components in plugins
A plugin can create TreantTree, ChartJs and GraphView like any other Vaadin component: they are classes of the
application, and their library comes with the application's frontend bundle. What a plugin can't do is add an npm
package or a JavaScript module to that bundle - it is built with the application. Libraries for plugins therefore have
to be added here, as a component. The only frontend resource a plugin can add is CSS (UI extensions,
ExtensionHasCss); plugins run their own small scripts with executeJs.
Vaadin add-ons
Besides the libraries above, the application uses two Vaadin add-ons (Maven dependencies, no npm):