Skip to main content

Extending Hub GUI

Solution GUI projects (e.g. io-oidis-{solution}-gui) depend on io-oidis-hub-gui and extend it with project-specific dashboard panels, models, menu items, and connectors. This page describes the available extension points and patterns.

Extension Points Overview​

WhatHowWhere
Dashboard panelsOverride getTabsConfiguration() on DashboardPage subclassPages/DashboardPage.ts
ModelsExtend hub-gui model classes with project-specific fieldsModels/
ConnectorsExtend hub-gui connector with project-specific backend methodsConnectors/
Menu itemsAutomatically created from tab configuration with scope propertyVia ITabDescriptor.scope
Page layoutsExtend StickyHeaderLayout or MenuPageLayoutPrimitives/
ValidatorsAdd project-specific DAO validatorsDAO/Validators/
BindingsCreate project-specific UI interaction handlersBindings/

Adding a Dashboard Panel​

This is the most common extension. A new dashboard panel requires three files: a panel view, a panel controller, and a modified DashboardPage.

Step 1: Create the Panel View​

import { BasePanel } from "@io-oidis-gui/Io/Oidis/Gui/Bootstrap/Primitives/BasePanel.js";
import { Button } from "@io-oidis-usercontrols/Io/Oidis/UserControls/Bootstrap/UserControls/Button.js";

export class MyToolPanel extends BasePanel {
public readonly refreshButton : Button;

constructor() {
super();
this.refreshButton = new Button();
}

protected innerHtml() : string {
return `
<div class="container-fluid">
<div class="row">
<div class="col">
<button class="btn btn-primary" data-oidis-bind="${this.refreshButton}">Refresh</button>
</div>
</div>
<div class="row">
<div class="col" id="content">
<!-- panel content -->
</div>
</div>
</div>`;
}
}

Step 2: Create the Panel Controller​

import { BasePanelController } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Primitives/BasePanelController.js";
import { EventsBinding } from "@io-oidis-gui/Io/Oidis/Gui/Bindings/EventsBinding.js";
import { MyToolPanel } from "../Panels/Dashboard/MyToolPanel.js";
import { MyConnector } from "../Connectors/MyConnector.js";

export class MyToolPanelController extends BasePanelController {
private readonly connector : MyConnector;

constructor() {
super();
this.connector = MyConnector.getClient();
}

public async FetchData($instance : MyToolPanel) : Promise<void> {
const data : any = await this.connector.GetData();
// populate panel with data
}

protected async beforeLoad($instance : MyToolPanel) : Promise<void> {
await super.beforeLoad($instance);
EventsBinding.SingleClick($instance.refreshButton, async () : Promise<void> => {
await this.FetchData($instance);
});
}
}

Step 3: Register in DashboardPage​

Override getTabsConfiguration() in your solution's DashboardPage:

import { DashboardPage as Parent } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Pages/DashboardPage.js";
import { ITabDescriptor } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Primitives/BasePageLayout.js";
import { MenuItemScope } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Panels/Dashboard/MenuPanel.js";
import { MyToolPanel } from "../Panels/Dashboard/MyToolPanel.js";
import { MyToolPanelController } from "../Controllers/Panels/MyToolPanelController.js";

export class DashboardPage extends Parent {
protected getTabsConfiguration() : ITabDescriptor[] {
return {
...super.getTabsConfiguration(),
myTool: {
name : "My Tool",
icon : `<svg>...</svg>`,
scope : MenuItemScope.ADMINS,
autoReload: true,
content : MyToolPanel,
controller: MyToolPanelController
}
};
}
}

The scope property controls menu visibility:

  • MenuItemScope.FOR_ALL — visible to all dashboard users
  • MenuItemScope.ADMINS — admin group only
  • MenuItemScope.SYS_ADMINS — system admin only
  • MenuItemScope.MOBILE_ONLY — mobile devices only
  • MenuItemScope.HIDDEN — accessible via URL but not in menu

Tab Properties Reference​

PropertyDefaultEffect
autoReloadfalseRe-fetches data when tab is re-selected
reloadOnRestartfalseRe-fetches data when server restart notification is received
lazyLoadtruePanel added to DOM only on first selection
isDefaultfalseSelected when dashboard loads
enabledtrueIf false, tab is registered but not shown
declaredBy—Override property name on page instance (for controller access)
warning

The deprecated registerTool() method on DashboardPageController still works but should not be used in new code. It adds tabs at runtime during beforeLoad(), which is less predictable than the getTabsConfiguration() override.

Extending Models​

Hub-gui models (User, Group, Organization) can be extended with project-specific fields:

import { User as Parent } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Models/User.js";
import { Property } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";

export class User extends Parent {
@Property()
public CustomField : string;

@Property()
public ProjectSpecificData : any;
}

The extended model inherits all parent properties and validators. If the solution has its own database collection, use @Entity() decorator. If it extends the hub's collection, no decorator is needed — the fields are added to the existing document.

Extending Connectors​

Add project-specific backend methods by extending hub-gui connectors:

import { AuthManagerConnector as Parent } from
"@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Connectors/AuthManagerConnector.js";

export class AuthManagerConnector extends Parent {

public static getClient() : AuthManagerConnector {
return <AuthManagerConnector>super.getClient();
}

public async FindProjectData($filter : any) : Promise<any> {
return this.asyncInvoke("FindProjectData", $filter);
}
}

The extended connector must match a backend connector with the corresponding @Extern() method in the solution's server package.

Overriding Page Controllers​

Solution projects can override page controllers to customize behavior:

import { DashboardPageController as Parent } from
"@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Controllers/Pages/DashboardPageController.js";
import { Resolver } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import { DashboardPage } from "../Pages/DashboardPage.js";

@Resolver("/dashboard", ["/tools", "/tools/{toolId}", "/backdoorLogin"])
export class DashboardPageController extends Parent {

constructor() {
super();
this.setInstanceOwner(new DashboardPage());
}
}

The @Resolver decorator on the subclass replaces the parent's route registration. The new controller's DashboardPage (with overridden getTabsConfiguration()) provides the extended tab set.

Overriding UserContext​

To extend the authentication context with project-specific state:

import { UserContext as Parent } from
"@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Structures/UserContext.js";

export class UserContext extends Parent {
public HasProjectPermission : boolean;

public async Load() : Promise<void> {
await super.Load();
// check project-specific permissions
}
}

Then override initContext() in your solution's Loader:

protected initContext() : UserContext {
return new UserContext();
}

DAO Validators​

Hub GUI includes reusable input validators in DAO/Validators/:

ValidatorPurpose
EmailValidatorEmail format validation
PhoneValidatorPhone number format validation
PasswordValidatorPassword strength and format validation
RangeValidatorNumeric range validation
RegexValidatorCustom regex pattern validation
EnumValidatorEnum value validation

These validators are used with the ValidationBinding from io-oidis-usercontrols to validate form inputs before submission. Solution projects can add custom validators following the same pattern.

QR Codes, Barcodes and Camera Scanning​

Utils/BarCodeEngine generates QR codes and barcodes and reads them with the camera. Each feature needs its library import in package.conf.jsonp and its page content bundle applied in the solution FrontEndLoader.setContent():

FeatureImport (dependencies/io-oidis-hub-gui/bin/project/configs/…)Bundle
Generate(canvas, value, {format: "QRCODE"})nodeQRcode.jsonp (loaded by the Hub itself)PageContentBundleType.QRCODE
Generate(target, value, IBarCodeOptions)jsBarcode.jsonpPageContentBundleType.BARCODE
Init(element), Start(onDetect, cameraId?), Stop()html5QRcode.jsonp (npm html5-qrcode)PageContentBundleType.CODE_SCANNER
// FrontEndLoader.setContent()
PageContentBundles.getInstance().Apply(PageContentBundleType.CODE_SCANNER);
  • A new import brings an npm dev dependency, so the library file exists only after oidis install; until then the script 404s and typeof Html5Qrcode === "undefined" — check it before offering a scan button.
  • Ready-made UI: Dialogs/ScannerDialog (camera selector, reading a code from a photo, notices; override localization) with Bindings/ScannerBinding. Bind($userControls, $onFind, $options?) passes the start buttons, which open the dialog and start the camera; the chosen camera is remembered per browser. Without options only numeric package labels "<id>" / "<id> <package>/<count>" reach $onFind (ScannerBinding.ParsePackageCode); with {onData: async ($data) => …} every code (QR link, EAN, Code 128) is passed as trimmed text. Call setState(true) to close the dialog and release the camera. The binding creates the BarCodeEngine only on the first scan, so it can be constructed on pages without the library.
  • Camera access needs a secure context (HTTPS or localhost) and navigator.mediaDevices.getUserMedia.
  • Hardware barcode readers usually type like a keyboard (quick keystrokes ending with Enter), so a focused text field with an Enter handler works without any library.

Color Scheme Choice​

Panels/ColorSchemePanel with Controllers/Panels/ColorSchemePanelController is the reusable "Color scheme" section: three options (follow the system, light, dark), applied to the Bootstrap color mode immediately and stored in the account of the signed-in user.

  • It is off by default. The Hub MyProfilePanel renders the section only when the target declares target.colorSchemeSupported: true (see Configuration Reference) — a theme without a dark appearance must not offer the choice. The same flag gates BasePageLayoutController.applyStoredColorScheme(), which applies the stored scheme on page load and re-reads it when the signed-in user changes.
  • Storage: AuthManager.getColorScheme() / AuthManager.setColorScheme($scheme) (AuthMethodCategory.ONLY_FOR_AUTHORIZED_USERS) read and write User.ColorScheme in the user collection. There is no target-user parameter, so a signed-in user can only change their own account. A missing or unknown value reads as ColorScheme.SYSTEM; without a signed-in user setColorScheme() stores nothing and returns false, and the panel then says that the scheme applies to the session only.
  • Values: Enums/ColorScheme — LIGHT 0, DARK 1, SYSTEM 2, plus ColorScheme.Normalize($value) shared by the backend and the browser.
  • Applying a scheme: Utils/ColorSchemeManager — Apply($scheme) sets the Bootstrap color mode through ElementManager.setTheme() and keeps following prefers-color-scheme under SYSTEM, getCurrent() returns the scheme applied last and IsDark() tells whether the page is rendered dark. Use it instead of calling setTheme() directly, for example to pick a chart or map palette.
  • The user menu no longer switches the theme. The Light / Dark items were removed from the Hub HeaderPanel and the dashboard header; headerPanel.lightTheme / darkTheme stay as deprecated controls so an application that renders them in its own header markup still compiles. Applications should drop them and rely on the profile section instead.

Translating the section​

The panel keeps English strings in localization (IColorSchemeLocalization). Override them in a subclass and return it from MyProfilePanel.getColorSchemePanelInstance():

import { ColorSchemePanel as Parent } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Panels/ColorSchemePanel.js";

export class ColorSchemePanel extends Parent {

constructor() {
super();

this.localization = {
title : "Barevné schéma",
storedHint : "Volba se ukládá k vašemu účtu.",
sessionHint : "Schéma platí jen pro tuto relaci, k účtu se neuložilo.",
failedHint : "Schéma se použilo, ale uložení k účtu se nezdařilo.",
systemOption: "Podle systému",
lightOption : "Světlé",
darkOption : "Tmavé"
};
}
}
// MyProfilePanel of the solution
protected getColorSchemePanelInstance() : ColorSchemePanel {
return new ColorSchemePanel();
}

Placing the section somewhere else​

An application whose profile page is overloaded can bind the panel anywhere and drive it with the same controller — the panel does not depend on MyProfilePanel:

// panel of the application
public readonly colorScheme : ColorSchemePanel;
// innerHtml(): <div class="row" data-oidis-bind="${this.colorScheme}"></div>
// its controller
protected async resolver($instance : SettingsPanel) : Promise<void> {
await super.resolver($instance);
this.colorSchemeController = new ColorSchemePanelController($instance.colorScheme, this);
}

public async FetchData() : Promise<void> {
await this.colorSchemeController.FetchData();
}

Create the controller in resolver(), because it binds the options, and call FetchData() whenever the section becomes visible. Toast messages are delegated to the owning panel controller, so nothing else has to be wired. An application that renders the section regardless of target.colorSchemeSupported is responsible for its own theme supporting both appearances.


See also: Hub GUI Overview | Dashboard and Tools Menu | Connectors and API | Configuration Reference | GUI Panels and Controllers | ElementManager | Decorators