Web UI Plugins
Dicoogle web user interface plugins, or just web plugins, are frontend-oriented pluggable components that live in the web application. The first section of this page is a tutorial that will guide you into making your first Dicoogle web plugin: a menu component to show a list of problems in the PACS. The second section will provide additional details about integrating web plugins and the APIs made available to them.
On web UI plugin support
The Dicoogle web UI plugin architecture is currently not under the project's stability guarantees. That is, each new release of Dicoogle might not be fully compatible with all web plugins. Features known to work well with the latest stable release of Dicoogle will be documented here in the Learning Pack. When working with development versions, the README pages in the webcore sub-project will be more up-to-date with changes in web plugin support.
Setting up a project
We will start by creating a Dicoogle web plugin project. Before we start, we must fulfill the following requirements:
- Node.js (LTS or Stable) ─ required for executing the building tools.
- npm (at least version 6 required) ─ the JavaScript package manager, usually installed alongside Node.js.
Now, we will need two components to generate the barebones web plugin project. The first one is the executable for Yeoman, a project scaffolding application with a generator ecosystem. The second one is a Dicoogle web plugin project generator, developed specifically to facilitate the development of this kind of plugins.
Install the following two packages globally, using the following command:
npm install -g yo generator-dicoogle-webplugin
Installing packages globally might not work immediately.
On Unix systems, you may need to fix the npm permissions. Although it is not recommended, you can also execute the command as super user (with sudo
).
While still on a command line, execute the following commands:
mkdir webplugin-healthcheck
cd webplugin-healthcheck
yo dicoogle-webplugin
The application will now be asking you a series of questions about the project.
- The project name will be the name of the npm project, and also the unique name of the plugin. We can leave the default by pressing Enter.
- The description is just a small text about the plugin, and is completely optional.
- Next you will be asked about the type of web plugin. For this example, we will select the
menu
type. - Afterwards, you may need to choose whether to generate a JavaScript or a TypeScript project. An ECMAScript2016+ project with Babel will include Babel to guarantee the existence of features that were already standardized in ECMAScript. A TypeScript project will have its source code in TypeScript, and will be configured to use a TypeScript compiler instead. Either one will work fine for this project, so choose the one which you are the most comfortable with.
- The minimum supported version of Dicoogle can also be specified. The higher the version, the more mechanisms will be provided. When developing for the latest version, pick the most recent one of the options given.
- The caption is a label that is shown in the web application. We will set this one to “Health Check”.
- Finally, you are requested additional information about the project, which can be added in case of the project being put into a public repository. They are all optional.
After the process is complete, you will have a brand new project in your working directory.
Building and installing
Before we make the necessary changes, let us see whether the created web plugin works. First we build the project:
npm install
This will yield, among others, a file named “module.js”. This one and “package.json” make the full plugin.
We will now install this plugin as a standalone web plugin. Create a folder “WebPlugins” in your “DicoogleDir” folder. Afterwards, create a directory “healthcheck” in “WebPlugins” and copy the two files above into this folder. The directory tree should look like this:
DicoogleDir
├── Plugins
| └── ...
├── WebPlugins
| └── healthcheck
| ├── package.json
| └── module.js
├── storage
| └── ...
├── ...
└── dicoogle.jar
Start Dicoogle and enter the web application. Our plugins should now appear on the sidebar.
Once we know that it works, it’s time to head back to our troubleshoot project.
Implementing a Dicoogle troubleshooting panel
At this point, we now want to implement the intended functionality The plugin should show a few boxes depending on potential issues found in the server:
- Whether no storage provider is installed;
- Whether no query or indexing provider is installed;
- Whether some of the plugins are dead (did not load properly).
The main question that arises would be: Where do I implement that? Let’s have a look at the generated source code in “src/index.js” (assuming the Babel project, the TypeScript project would contain the file “src/index.ts” with similar content).
/* global Dicoogle */
export default class MyPlugin {
constructor() {
// TODO initialize plugin here
}
/**
* @param {DOMElement} parent
* @param {DOMElement} slot
*/
render(parent, slot) {
// TODO mount a new web component here
const div = document.createElement('div');
div.innerHTML = 'Hello, Dicoogle!';
parent.appendChild(div);
}
}
There may be many parts that are not quite understandable here, but the essentials are:
- The whole plugin is represented as a class, and this is the module’s default export. Typically, you do not have to touch this.
- The constructor can be used to initialize certain parts of the plugin before any rendering takes place. It is not always needed.
- The
render
method is the most important portion of the plugin: it is where new HTML elements are created and written to the web app’s document. The example shows how this can be done with the standard Document Object Model (DOM) Web API. - In order to develop plugins safely,
the elements should be attached as children to the
parent
element.
Instead of creating a div, we will create a header and other elements to provide information to the user.
render(parent, slot) {
// create header
const head = document.createElement('h3');
head.text = 'Health Check';
parent.appendChild(head);
// create main info span
const baseInfo = document.createElement('span');
this.baseInfo = baseInfo;
parent.appendChild(baseInfo);
// create list of issues found
this.ul = document.createElement('ul');
parent.appendChild(this.ul);
}
A new question should arise here: How do we interact with Dicoogle from here?
Interacting with Dicoogle
Interfacing with the Dicoogle instance is done through the Dicoogle client API, in the dicoogle-client
package.
The package can be included by independent applications.
But when developing web plugins, we don’t have to.
Instead, a global variable Dicoogle
is automatically exposed with all of the features.
The operations available are listed in the Dicoogle Client documentation.
In particular, we are looking for methods to retrieve information about the plugins:
Dicoogle.getPlugins()
─ to retrieve a detailed list of plugins installed.
With a bit of client-side programming, one may come up with something like this:
render(parent: HTMLElement, slot: SlotHTMLElement) {
// create header
const head = document.createElement('h3');
head.innerText = 'Health Check';
parent.appendChild(head);
// create main info span
const baseInfo = document.createElement('span');
baseInfo.innerText = 'Checking Dicoogle health...';
parent.appendChild(baseInfo);
// request for the full list of plugins
Dicoogle.getPlugins().then(({ plugins, dead }) => {
const problems = [];
// check for no storage
if (plugins.filter(p => p.type === 'storage').length === 0) {
problems.push("No storage providers are installed");
}
// check for no DICOM query provider
if (plugins.filter(p => p.type === 'query' && p.dim).length === 0) {
problems.push("No DICOM data query providers are installed");
}
// check for no DICOM index provider
if (plugins.filter(p => p.type === 'index' && p.dim).length === 0) {
problems.push("No DICOM data indexers are installed");
}
if (dead.length > 0) {
problems.push("The following plugins are dead: " + dead
.map(p => `${p.name} (${p.cause.message})`).join(', '))
}
// update DOM with problems
if (problems.length === 0) {
baseInfo.innerText = "\u2713 No issues were found!";
} else {
baseInfo.innerText = `\u26a0 There are ${problems.length} ${problems.length === 1 ? "issue" : "issues"} in this installation.`;
// create list of issues found
const ul = document.createElement('ul');
for (const problem of problems) {
// one list item per problem
let problemItem = document.createElement('li');
problemItem.innerText = problem;
ul.appendChild(problemItem);
}
parent.appendChild(ul);
}
});
}
Let’s repeat the installation process by running npm install
and copying the updated “module.js” file to the deployment folder. We may now enter the web application again and see that the changes have taken effect.
Web plugins are cached by the browser!
If you find that the plugins are not being updated properly, you may have to temporarily clean up or disable caching in your browser. This shouldn't come up as an issue in production, since web plugins do not change frequently.
Further information
The rest of this page contains further details about Dicoogle web plugins and how they work.
Dicoogle Webcore
The Dicoogle webcore is one of the components of the webapp that serves as a backbone to web UI plugins. The essence of this architecture is that Dicoogle web pages will contain stub slots where plugins can be attached to. The webcore implements this logic, and the source code can be found here.
Plugin descriptor
A descriptor takes the form of a “package.json”, an npm
package descriptor, containing at least these attributes:
name
: the unique name of the plugin (must be compliant with npm)version
: the version of the plugin (must be compliant with npm)description
(optional) : a simple, one-line description of the packagedicoogle
: an object containing Dicoogle-specific information:caption
(optional, defaults to name) : an appropriate title for being shown as a tab (or similar) on the web pageslot-id
: the unique ID of the slot where this plugin is meant to be attachedmodule-file
(optional, defaults to “module.js”) : the name of the file containing the JavaScript module
In addition, these attributes are recommended:
author
: the author of the plugintags
: the tags “dicoogle” and “dicoogle-plugin” are recommendedprivate
: if you do not intend to publish the plugin into an npm repository, set this totrue
.
An example of a valid “package.json”:
{
"name" : "dicoogle-cbir-query",
"version" : "0.0.1",
"description" : "CBIR Query-By-Example plugin",
"author": "John Doe <jdoe@somewhere.net>",
"tags": ["dicoogle", "dicoogle-plugin"],
"dicoogle" : {
"caption" : "Query by Example",
"slot-id" : "query",
"module-file" : "module.js"
}
}
Module
In addition, a JavaScript module must be implemented, containing the entire logic and rendering of the plugin.
Modules are meant to work independently, but can have embedded libraries if so is desired. In addition, if the underlying web page is known to contain specific libraries, then these can also be used without being embedded. This is particularly useful to avoid replicating dependencies and prevent modules from being too large.
Below is an example of a plugin module.
module.exports = function() {
// ...
this.render = function(parent, slot) {
var e = document.create('span');
e.innerHTML = 'Hello Dicoogle!';
parent.appendChild(e);
};
};
Exporting a class in ECMAScript 2015 also works, since classes are mostly syntatic sugar for ES5 constructors. See the following example using the standard ECMAScript module system:
export default class MyPluginModule() {
render(parent, _slot) {
let e = document.create('span');
e.innerHTML = 'Hello Dicoogle!';
parent.appendChild(e);
}
};
This one will work if it converted to CommonJS with the right configuration. For example:
- When using Webpack,
the
output.libraryTarget
property (output.library.type
since Webpack 5) should be set to'commonjs2'
. - When using Parcel,
the target’s
outputFormat
property should be'commonjs'
and thecontext
should be “browser”. - If using Babel directly, ensure that
targets.esmodules
is set tofalse
.
All modules will have access to the Dicoogle
plugin-local alias for interfacing with Dicoogle.
Other REST services exposed by Dicoogle are easily accessible with request(...)
.
See the Dicoogle JavaScript client package and the Dicoogle
Web API section below for a more thorough documentation.
Types of Web Plugins
As previously mentioned, we are requested to specify a a type of plugin, often with the “slot-id” property. This type defines how webplugins are attached to the application. The following kinds of web UI plugins are supported:
- menu: Menu plugins are used to augment the main menu. A new entry is added to the side bar (named by the plugin’s caption property), and the component is created when the user navigates to that entry.
- result-option: Result option plugins are used to provide advanced operations to a result entry. If the user activates “Advanced Options” in the search results view, these plugins will be attached into a new column, one for each visible result entry.
- result-batch: Result batch plugins are used to provide advanced operations over an existing list of results. These plugins will attach a button (named with the plugin’s caption property), which will pop-up a division below the search result view.
- settings: Settings plugins can be used to provide addition management information and control. These plugins will be attached to the “Plugins & Services” tab in the Management menu.
Module format and compatibility
Stick to generator-dicoogle-webplugins and you will be fine
By using the project scaffolding tool, you will have a complete project with all the necessary operations to build a compatible module without any other concerns.
The details described in this section are for those who wish to understand how it works in greater detail.
The script file module.js, that will be loaded by Dicoogle, must exist as a Node.js compatible module. This allows the Dicoogle back-end to encapsulate it with the boilerplate that registers the plugin via the Dicoogle webcore system by the web app.
The exported module must be a single constructor function or class,
in which instances must have a render(parent, slot)
method:
class MyPlugin {
/** Render and attach the contents of a new plugin instance to the given DOM element.
* @param {DOMElement} parent the parent element,
* created exclusively for each plugin component
* @param {DOMElement} slot the DOM element of the Dicoogle slot
*/
render(parent, slot) {
// ...
}
}
The export can either be made as an assignment to module.exports
:
module.exports = MyPlugin;
Or through the export named default
with the ES module interoperability flag:
module.exports = {
__esModule: true,
default: MyPlugin,
};
The developer can make multiple node-flavored CommonJS modules
and use tools like Webpack to bundle them and embed dependencies
into a single module.js file.
However, some dependencies can be obtained from Dicoogle without embedding them.
In particular, modules such as "react"
, "react-dom"
, and "dicoogle-client"
can be imported externally through require
,
and so should be marked as external dependencies.
The developer may also choose to deliver a UMD module, although this is not necessary.
On support for React components
The latest version allows users to render React elements by returning them from the render method instead of attaching
bare DOM elements to the parent div. However, this feature is unstable and known not to work very well. Future versions
may allow a smooth approach to developing web plugins in a pure React environment. In the meantime, it is possible to
use React by calling ReactDOM.render
directly on parent
.
Dicoogle Web API
Either require
the dicoogle-client
module (if the page supports the operation) or use the alias Dicoogle
to
access and perform operations on Dicoogle and the page’s web core. All methods described in
dicoogle-client
are available. Furthermore, the web
core injects the following methods:
issueQuery : function(query, options, callback)
Issue a query to the system. This operation is asynchronous and will automatically issue back a result exposal to the page’s result module. The query service requested will be “search” unless modified with the overrideService option.
- query an object or string containing the query to perform
- options an object containing additional options (such as query plugins to use, result limit, etc.)
- [overrideService] {string} the name of the service to use instead of “search”
- callback an optional callback function(error, result)
addEventListener : function(eventName, fn)
Add an event listener to an event triggered by the web core.
- eventName : the name of the event (can be one of ‘load’,’menu’ or a custom one)
- fn : a callback function (arguments vary) –
function(...)
addResultListener : function(fn)
Add a listener to the ‘result’ event, triggered when a query result is obtained.
- fn :
function(result, requestTime, options)
addPluginLoadListener : function(fn)
Add a listener to the ‘load’ event, triggered when a plugin is loaded.
- fn :
function(Object{name, slotId, caption})
addMenuPluginListener : function(fn)
Add a listener to the ‘menu’ event, triggered when a menu plugin descriptor is retrieved. This may be useful for a web page to react to retrievals by automatically adding menu entries.
- fn :
function(Object{name, slotId, caption})
emit: function(eventName, ...args)
Emit an event through the webcore’s event emitter.
- eventName : the name of the event
- args : variable list of arguments to be passed to the listeners
emitSlotSignal: function(slotDOM, eventName, data)
Emit a DOM custom event from the slot element.
- slotDOM : the slot DOM element to emit the event from
- name : the event name
- data : the data to be transmitted as custom event detail
Webcore Events
Full list of events that can be used by plugins and the webapp. (Work in Progress)
- “load” : Emitted when a plugin package is retrieved.
- “result” : Emitted when a list of search results is obtained from the search interface.
Further details about web UI plugins may be obtained in the webcore project.