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
| What | How | Where |
|---|---|---|
| Dashboard panels | Override getTabsConfiguration() on DashboardPage subclass | Pages/DashboardPage.ts |
| Models | Extend hub-gui model classes with project-specific fields | Models/ |
| Connectors | Extend hub-gui connector with project-specific backend methods | Connectors/ |
| Menu items | Automatically created from tab configuration with scope property | Via ITabDescriptor.scope |
| Page layouts | Extend StickyHeaderLayout or MenuPageLayout | Primitives/ |
| Validators | Add project-specific DAO validators | DAO/Validators/ |
| Bindings | Create project-specific UI interaction handlers | Bindings/ |
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 usersMenuItemScope.ADMINS— admin group onlyMenuItemScope.SYS_ADMINS— system admin onlyMenuItemScope.MOBILE_ONLY— mobile devices onlyMenuItemScope.HIDDEN— accessible via URL but not in menu
Tab Properties Reference
| Property | Default | Effect |
|---|---|---|
autoReload | false | Re-fetches data when tab is re-selected |
reloadOnRestart | false | Re-fetches data when server restart notification is received |
lazyLoad | true | Panel added to DOM only on first selection |
isDefault | false | Selected when dashboard loads |
enabled | true | If false, tab is registered but not shown |
declaredBy | — | Override property name on page instance (for controller access) |
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/:
| Validator | Purpose |
|---|---|
EmailValidator | Email format validation |
PhoneValidator | Phone number format validation |
PasswordValidator | Password strength and format validation |
RangeValidator | Numeric range validation |
RegexValidator | Custom regex pattern validation |
EnumValidator | Enum 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():
| Feature | Import (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.jsonp | PageContentBundleType.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 andtypeof Html5Qrcode === "undefined"— check it before offering a scan button. - Ready-made UI:
Dialogs/ScannerDialog(camera selector, reading a code from a photo, notices; overridelocalization) withBindings/ScannerBinding.Bind($userControls, $onFind, $options?)passes thestartbuttons, 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. CallsetState(true)to close the dialog and release the camera. The binding creates theBarCodeEngineonly 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
MyProfilePanelrenders the section only when the target declarestarget.colorSchemeSupported: true(see Configuration Reference) — a theme without a dark appearance must not offer the choice. The same flag gatesBasePageLayoutController.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 writeUser.ColorSchemein theusercollection. There is no target-user parameter, so a signed-in user can only change their own account. A missing or unknown value reads asColorScheme.SYSTEM; without a signed-in usersetColorScheme()stores nothing and returnsfalse, and the panel then says that the scheme applies to the session only. - Values:
Enums/ColorScheme—LIGHT0,DARK1,SYSTEM2, plusColorScheme.Normalize($value)shared by the backend and the browser. - Applying a scheme:
Utils/ColorSchemeManager—Apply($scheme)sets the Bootstrap color mode throughElementManager.setTheme()and keeps followingprefers-color-schemeunderSYSTEM,getCurrent()returns the scheme applied last andIsDark()tells whether the page is rendered dark. Use it instead of callingsetTheme()directly, for example to pick a chart or map palette. - The user menu no longer switches the theme. The
Light/Darkitems were removed from the HubHeaderPaneland the dashboard header;headerPanel.lightTheme/darkThemestay 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