graphdb-workbench

Developers Guide

Extending the GraphDB Workbench

Plugin system

Since v1.2, GraphDB workbench features a plugin system which allows components to be plugged in without introducing coupling between new and existing components. The new system allows extending or replacing existing components, introduction of new single or compositional components. All this could be achieved without any changes in the rest of the system.

Currently the new system is integrated in the workbench main components registration. These are the components which implement the main workbench views (extension point route) and their respective main menu entries (extension point main.menu). In next versions more extension points might be introduced.

What is the plugin system and how a developer can use it?

The plugin system consist of four components.

The PluginRegistry which is a service that has a role to maintain a runtime registry of all registered to given extension point plugins. It has the following interface:

PluginRegistry {
    add(extensionPoint:String, pluginDefinition:[Object|Array]),
    get(extensionPoint:String):Array,
    clear(extensionPoint:String),
    listModules()
}

The second component is the plugin definition which a developer can define for each extension point where he needs new behavior or customization. When there is a need given component to be extended or customized, the developer declares an extension point which is the contract to which plugins can be registered. Every plugin should conform to that contract. Plugin definitions are javascript files with mandatory name plugin.js.

Example of a plugin definition:

// src/js/angular/autocomplete/plugin.js
PluginRegistry.add('route', {
    'url': '/autocomplete',
    'module': 'graphdb.framework.autocomplete',
    'path': 'autocomplete/app',
    'chunk': 'autocomplete',
    'controller': 'AutocompleteCtrl',
    'templateUrl': 'pages/autocomplete.html',
    'title': 'Autocomplete index',
    'helpInfo': 'The Autocomplete index is used for automatic ...'
});

In a single plugin.js definition file can be registered plugins to multiple extension points.

The above plugin.js definition can be extended like this:

PluginRegistry.add('route', {
    'url': '/autocomplete',
    'module': 'graphdb.framework.autocomplete',
    'path': 'autocomplete/app',
    'chunk': 'autocomplete',
    'controller': 'AutocompleteCtrl',
    'templateUrl': 'pages/autocomplete.html',
    'title': 'Autocomplete index',
    'helpInfo': 'The Autocomplete index is used for automatic completion of URIs in the SPARQL editor and the View resource page. Use this view to enable or disable the index and check its status.'
});

PluginRegistry.add('main.menu', {
    'items': [
        {label: 'Setup', href: '#', order: 5, role: 'IS_AUTHENTICATED_FULLY', icon: "icon-settings"},
        {label: 'Autocomplete', href: 'autocomplete', order: 40, parent: 'Setup', role: "IS_AUTHENTICATED_FULLY"}
    ]
});

Plugin definitions can have the following optional attributes: order, priority, disabled.

The third component from the plugin system is the plugins.js file which is autogenerated and is composed from all plugin.js files during the workbench build. Usually developers don’t need to touch this file.

The last part are the extension points which the developer implements in the application code. An extension point is the place where plugins are loaded and eventually executed.

Below is an example of extension point in the workbench which allows registering modules that can be accessed by navigating to different url (routes).

// src/app.js
// 1. Get all registered extension for the "route" extension point. 
let routes = PluginRegistry.get('route');
// 2. Loop through all extensions
routes.forEach(function (route) {
    // 3. Register every extension with the $routeProvider
    $routeProvider.when(route.url, {
        controller: route.controller,
        templateUrl: route.templateUrl,
        title: route.title,
        helpInfo: route.helpInfo,
        reloadOnSearch: route.reloadOnSearch !== undefined ? route.reloadOnSearch : true
    });
});

How does the plugin system work?

This allows the plugins to be registered runtime in the registry immediately after the web 
application is loaded by issuing calls to `PluginRegistry.add(extensionPoint, pluginDefintion)` 
method.

## Color themes

The workbench allows custom color themes to be provided by developers. Themes are implemented as plugins
and can be registered to the `themes` extension point. Any custom theme must be placed in the project's
src folder a file named `plugin.js`. Example theme plugin can be seen below:

```javascript
PluginRegistry.add('themes', {
    // The name of the theme. Must contain only lowercase letters, hyphen, underscore. This is the differentiator
    // property for all registered themes.
    'name': 'default-theme',
    // The theme label or a key for a label from i18n resource bundle.
    'label': 'security.workbench.settings.theme.default-theme',
    // CSS variables, "foo: bar" becomes "--foo: bar"
    'variables': {
        // Primary color, like a main brand color. This is in a HSL format composed by three values below
        'primary-color-hue': '13.4',
        'primary-color-saturation': '87.9%',
        'primary-color-lightness': '33%',
        // Secondary color, like a contrast main brand color. This is in a HSL format composed by three values below
        'secondary-color-hue': '207.3',
        'secondary-color-saturation': '100%',
        'secondary-color-lightness': '19.4%',
        // Tertiary color, like a complimentary color. This is in a HSL format composed by three values below
        'tertiary-color-hue': '174.6',
        'tertiary-color-saturation': '97.7%',
        'tertiary-color-lightness': '33.5%',
        // A color used for the font/svg icons when placed on a primary color background.
        'icon-on-primary-color': 'rgba(255, 255, 255, 0.8)',
        'gray-color': '#97999C',
        'gray-color-dark': '#575757',
        // Colors for the toastr notifications, the tag-xxx and the text-xxx classes in any of their four states
        // (i.e. dark colored things)
        'color-danger-dark': 'hsl(353, 78%, 36%)',
        'color-success-dark': 'hsl(var(--tertiary-color-hue), var(--tertiary-color-saturation), calc(var(--tertiary-color-lightness)*0.5))',
        'color-warning-dark': 'var(--primary-color-dark)',
        'color-info-dark': 'var(--secondary-color-light)',
        // Colors for the alert boxes (i.e. light colored things).
        // Success and info are the same color since we don't use success much if at all
        'color-danger-light': '#a4142433',
        'color-success-light': 'hsla(var(--tertiary-color-hsl), 0.15)',
        'color-warning-light': 'hsla(var(--primary-color-hsl), 0.07)',
        'color-info-light': 'hsla(var(--tertiary-color-hsl), 0.15)',
        'color-help-light': 'hsla(var(--secondary-color-hsl), 0.1)',
        // Colors for the logo - logo proper, text in logo, logo background
        'logo-color': 'var(--primary-color-light)',
        'logo-text-color': 'white',
        'logo-background-color': 'var(--secondary-color-dark)'
    },
    // Dark theme
    'dark': {
        'variables': {
            // Dark definition variables that affect things at a global scale
            'body-filter': 'invert(95%) hue-rotate(180deg)',
            'html-background': '#0d0d0d',
            'media-filter': 'invert(100%) hue-rotate(180deg)',
            'alert-filter': 'contrast(2)',
            'checkbox-filter': 'invert(100%) hue-rotate(180deg)',
            'toast-filter': 'invert(95%) hue-rotate(180deg) contrast(1.2)',
            // Slightly different colors that work better in dark mode
            'primary-color-lightness': '60%',
            'secondary-color-saturation': '70%',
            'color-warning-light': 'hsla(var(--primary-color-hsl), 0.15)',
            'logo-color': 'var(--primary-color-dark)'
        },
        // CSS properties, "foo: bar" becomes "foo: bar"
        'properties': {
            // Notify browser that we support dark theme, makes checkboxes look better
            'color-scheme': 'light dark'
        }
    }
});

The plugin definition is compiled to a stylesheet and embedded in the html document

:root {
    --primary-color-hue: 13.4;
    --primary-color-saturation: 87.9%;
    --primary-color-lightness: 33%;
    --primary-color-hsl: var(--primary-color-hue), var(--primary-color-saturation), var(--primary-color-lightness);
    --primary-color: hsl(var(--primary-color-hsl));
    --primary-color-light: hsl(var(--primary-color-hue), var(--primary-color-saturation), calc(var(--primary-color-lightness)*1.2));
    --primary-color-dark: hsl(var(--primary-color-hue), var(--primary-color-saturation), calc(var(--primary-color-lightness)*0.8));

    --secondary-color-hue: 207.3;
    --secondary-color-saturation: 100%;
    --secondary-color-lightness: 19.4%;
    --secondary-color-hsl: var(--secondary-color-hue), var(--secondary-color-saturation), var(--secondary-color-lightness);
    --secondary-color: hsl(var(--secondary-color-hsl));
    --secondary-color-light: hsl(var(--secondary-color-hue), var(--secondary-color-saturation), calc(var(--secondary-color-lightness)*1.2));
    --secondary-color-dark: hsl(var(--secondary-color-hue), var(--secondary-color-saturation), calc(var(--secondary-color-lightness)*0.8));

    ...
}

:root.dark {
    --body-filter: invert(95%) hue-rotate(180deg);
    --html-background: #0d0d0d;
    --media-filter: invert(100%) hue-rotate(180deg);
    --alert-filter: contrast(2);
    --checkbox-filter: invert(100%) hue-rotate(180deg);
    --toast-filter: invert(95%) hue-rotate(180deg) contrast(1.2);
    
    ...
    
    color-scheme: light dark;
}

All available registered themes are loaded using the PluginsRegistry and displayed in a combobox in “my settings” page. The definition of the selected in the ‘my settings’ page and saved theme is loaded and applied by default when the workbench is opened. If no theme is selected and saved in the local storage, then the default-theme is applied. All properties in the definition are mandatory. Definitions with missing properties are rejected, and an error will be reported in the browser log. Theme plugins validation happens when definitions are loaded through the PluginRegistry by the “my settings” page controller. The themes selector menu is in a widget on that page and it lists all available registered theme plugins for the user to select from. There are two prebuilt themes in the workbench. The default-theme is carefully crafted so that the colors used in the theme comply with the WCAG AAA color contrast standard. The second provided theme uses the Ontotext brand colors and does not comply with the WCAG color standard.

Bundling

The workbench application consists of many resources: script files, stylesheets, images, fonts. In a non optimized build this would lead to hundreds of http requests. Nowadays web applications are usually optimized by bundling their resources and minifying them. The reason for this is to have significantly fewer http requests which leads to faster initial load and less traffic.

Bundling of the workbench is done with webpack. It’s configured with three config files:

The common config contains shared configuration for the other two.

There are two ways the application to be built. First is for production and the other is for dev. See the Build and Dev server sections above.

The code for production is build in in the /dest folder. The bundling covers the following tasks: