View on GitHub

Resources for users and developers of Dicoogle

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:

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 package
  • dicoogle : 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 page
    • slot-id : the unique ID of the slot where this plugin is meant to be attached
    • module-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 plugin
  • tags : the tags “dicoogle” and “dicoogle-plugin” are recommended
  • private : if you do not intend to publish the plugin into an npm repository, set this to true.

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:

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.