diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 7c9f498d..4fdd5cc4 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -128,8 +128,62 @@ export default withMermaid({ ], }, { - text: "Registering a Microfrontend", - link: "/documentation/extended-guide/register-microfrontend", + text: "Frontend configuration", + link: "/documentation/extended-guide/frontend-configuration/", + items: [ + { + text: "Luigi general settings", + link: "/documentation/extended-guide/frontend-configuration/general-settings", + }, + { + text: "Luigi routing settings", + link: "/documentation/extended-guide/frontend-configuration/routing-settings", + }, + { + text: "Node change hook settings", + link: "/documentation/extended-guide/frontend-configuration/node-change-hook-settings", + }, + { + text: "Global search settings", + link: "/documentation/extended-guide/frontend-configuration/global-search-settings", + }, + { + text: "Header bar settings", + link: "/documentation/extended-guide/frontend-configuration/header-bar-settings", + }, + { + text: "User profile settings", + link: "/documentation/extended-guide/frontend-configuration/user-profile-settings", + }, + { + text: "Custom message listeners", + link: "/documentation/extended-guide/frontend-configuration/message-listeners", + }, + { + text: "Error component config", + link: "/documentation/extended-guide/frontend-configuration/error-config", + }, + { + text: "Authorization events", + link: "/documentation/extended-guide/frontend-configuration/luigi-auth-callbacks-settings", + }, + { + text: "Luigi global context", + link: "/documentation/extended-guide/frontend-configuration/luigi-global-context", + }, + { + text: "Luigi node context", + link: "/documentation/extended-guide/frontend-configuration/luigi-node-context", + }, + { + text: "Luigi node processing", + link: "/documentation/extended-guide/frontend-configuration/node-processing", + }, + { + text: "Global nodes", + link: "/documentation/extended-guide/frontend-configuration/global-nodes", + }, + ], }, { text: "Content Configuration", diff --git a/documentation/extended-guide/backend-configuration/index.md b/documentation/extended-guide/backend-configuration/index.md index dc0d195b..e15bf39c 100644 --- a/documentation/extended-guide/backend-configuration/index.md +++ b/documentation/extended-guide/backend-configuration/index.md @@ -1,7 +1,7 @@ # Backend configuration -The nest.js backend module is designed to accept a set of configurations, -allowing you to customize it to your needs and infrastructure. +The nest.js backend module available in **openmfp/portal-server-lib** is designed to accept a set of +configurations, allowing you to customize it to your needs and infrastructure. In this guide, you’ll learn: diff --git a/documentation/extended-guide/frontend-configuration/error-config.md b/documentation/extended-guide/frontend-configuration/error-config.md new file mode 100644 index 00000000..c79d15f6 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/error-config.md @@ -0,0 +1,81 @@ +# Customizing Entity Error Handling + +The `errorComponentConfig` option allows you to define custom navigation +behavior when the library fails to retrieve an entity configuration. +By default, the library displays a simple alert with the error message. +By providing this configuration, +you can instead redirect users to custom error pages or "not found" views. + +## ErrorComponentConfig Interface + +```ts +export interface ErrorComponentConfig { + /** + * Triggered when an entity configuration retrieval fails. + * @param entityDefinition The definition of the entity that failed to load. + * @param errorCode The HTTP status code returned by the server (e.g., 404, 500). + * @param additionalContext Optional metadata regarding the failure. + * @returns An array of LuigiNode objects to be displayed as a fallback. + */ + handleEntityRetrievalError( + entityDefinition: EntityDefinition, + errorCode: number, + additionalContext?: Record, + ): LuigiNode[]; +} +``` + +## Provide your implemented service + +```ts +import { ErrorComponentConfig, EntityDefinition, LuigiNode } from '@openmfp/portal-ui-lib'; + +const errorComponentConfig: ErrorComponentConfig = { + handleEntityRetrievalError: ( + entityDefinition: EntityDefinition, + errorCode: number, + additionalContext?: Record, + ): LuigiNode[] => { + if (errorCode === 404) { + return [ + { + pathSegment: 'not-found', + label: 'Not Found', + viewUrl: '/assets/not-found.html', + }, + ]; + } + + return [ + { + pathSegment: 'error', + label: 'Error', + viewUrl: `/assets/error.html?code=${errorCode}`, + }, + ]; + }, +}; +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { errorComponentConfig } from './error-config'; + +const portalOptions: PortalOptions = { + // Pass the configuration object directly + errorComponentConfig: errorComponentConfig, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); + +``` diff --git a/documentation/extended-guide/frontend-configuration/general-settings.md b/documentation/extended-guide/frontend-configuration/general-settings.md new file mode 100644 index 00000000..f4bf8557 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/general-settings.md @@ -0,0 +1,67 @@ +# Customizing Static Luigi Settings + +To customize the general UI settings provided by the underlying Luigi framework, +you must implement the `StaticSettingsConfigService` interface and provide your custom service +during the application bootstrap. This service allows you to override any default +[Luigi general settings](https://docs.luigi-project.io/docs/general-settings) +(like the header title, logo, or loading behavior). + +## StaticSettingsConfigService Interface + +The interface is defined as follows: + +```ts +export interface StaticSettingsConfigService { + getStaticSettingsConfig(): Promise; +} +``` + +## Provide your implemented service + +```ts +import { StaticSettingsConfigService } from '@openmfp/portal-ui-lib'; + +export class StaticSettingsConfigServiceImpl + implements StaticSettingsConfigService +{ + constructor() {} + + getStaticSettingsConfig(): Promise { + const logo = 'assets/my-logo.svg'; + + return { + header: { + title: 'My App', + logo: logo, + favicon: logo, + }, + appLoadingIndicator: { + hideAutomatically: false, + }, + links: [{ title: 'OpemMFP', link: 'https://openmfp.org/' }], + // ... the rest of the configuration + }; + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { StaticSettingsConfigServiceImpl } from './static-settings-config.service'; // Import your new service + +const portalOptions: PortalOptions = { + // Reference the service class here. The Portal UI Library will instantiate it. + staticSettingsConfigService: StaticSettingsConfigServiceImpl, +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` diff --git a/documentation/extended-guide/frontend-configuration/global-nodes.md b/documentation/extended-guide/frontend-configuration/global-nodes.md new file mode 100644 index 00000000..800caaee --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/global-nodes.md @@ -0,0 +1,90 @@ +# Defining Custom Global Nodes + +The `customGlobalNodesService` option allows you to define and inject global-level Luigi Nodes into +your application. These nodes are typically used for top-navigation items or +global utility links that remain accessible regardless of the current micro-frontend context. + +## CustomGlobalNodesService Interface + +The `CustomGlobalNodesService` interface requires an asynchronous method that returns an array of LuigiNode objects. + +```ts +export interface CustomGlobalNodesService { + /** + * Returns a Promise resolving to an array of LuigiNode objects + * to be added to the global navigation level. + */ + getCustomGlobalNodes(): Promise; +} +``` + +## Provide your implemented service + +In your implementation, you can define nodes that point to external links or internal view URLs. You can also +define specific entityType properties to control where these nodes appear in the Luigi shell. + +```ts +import { + LuigiNode, + CustomGlobalNodesService +} from '@openmfp/portal-ui-lib'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class CustomGlobalNodesServiceImpl implements CustomGlobalNodesService { + + /** + * Orchestrates the creation of global nodes. + */ + async getCustomGlobalNodes(): Promise { + return [ + this.createHelpCenterNode(), + this.createSettingsNode(), + ]; + } + + private createHelpCenterNode(): LuigiNode { + return { + label: 'Help Center', + entityType: 'global.topnav', // Categorizes the node for top navigation + url: '[https://docs.example.com](https://docs.example.com)', + // ... other luigi node properties + }; + } + + private createSettingsNode(): LuigiNode { + return { + label: 'Global Settings', + entityType: 'global.topnav', + pathSegment: 'global-settings', + urlSuffix: '/assets/views/settings.html', + icon: 'settings' + // ... other luigi node properties + }; + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { CustomGlobalNodesServiceImpl } from './global-nodes.service'; + +const portalOptions: PortalOptions = { + // Reference the service class here. The library handles instantiation. + customGlobalNodesService: CustomGlobalNodesServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); + +``` + diff --git a/documentation/extended-guide/frontend-configuration/global-search-settings.md b/documentation/extended-guide/frontend-configuration/global-search-settings.md new file mode 100644 index 00000000..5ad932ea --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/global-search-settings.md @@ -0,0 +1,80 @@ +# Customizing Luigi Global Search + +The `globalSearchConfigService` option allows you to define the behavior, appearance, +and event handling for the [Luigi global search](https://docs.luigi-project.io/docs/global-search). +By implementing this service, you can integrate +your own search provider logic directly into the portal header. + + +## GlobalSearchConfigService Interface + +The `GlobalSearchConfigService` interface requires the implementation of a single method +that returns the search configuration object. + +```ts +export interface GlobalSearchConfigService { + /** + * Must return a valid Luigi configuration object for global search. + */ + getGlobalSearchConfig(): any; +} +``` + +## Provide your implemented service + +```ts +import { GlobalSearchConfigService } from '@openmfp/portal-ui-lib'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class GlobalSearchConfigServiceImpl implements GlobalSearchConfigService { + + getGlobalSearchConfig() { + return { + // UI Setting: Centers the search input in the shell header + searchFieldCentered: true, + + // Define handlers for various search events + searchProvider: { + onEnter: (searchString: string) => { + console.log('Search triggered via Enter:', searchString); + // Add your search execution logic here + }, + onSearchBtnClick: (searchString: string) => { + console.log('Search button clicked:', searchString); + // Add your search execution logic here + }, + onEscape: () => { + console.log('Search escaped'); + // Logic to clear results or close the search overlay + }, + + // You can include additional Luigi search properties + } + }; + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { GlobalSearchConfigServiceImpl } from './global-search-config.service'; + +const portalOptions: PortalOptions = { + // Pass the class reference to the portal options + globalSearchConfigService: GlobalSearchConfigServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + diff --git a/documentation/extended-guide/frontend-configuration/header-bar-settings.md b/documentation/extended-guide/frontend-configuration/header-bar-settings.md new file mode 100644 index 00000000..bf56b84d --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/header-bar-settings.md @@ -0,0 +1,80 @@ +# Customizing the Header Bar + +The `headerBarConfigService` option allows you to configure the header bar and extend +[Luigi breadcrumbs](https://docs.luigi-project.io/docs/navigation-advanced?section=breadcrumbs). +Beyond standard breadcrumb settings, this service provides the +unique ability to inject custom HTML content into the left and right sides of the +header bar using renderer functions. + +## HeaderBarConfigService Interface + +```ts +export interface HeaderBarConfigService { + getBreadcrumbsConfig(): Promise; +} +``` + +## Provide your implemented service + +```ts +import { + HeaderBarConfigService, + HeaderBarConfig, +} from '@openmfp/portal-ui-lib'; + + +export class HeaderBarConfigServiceImpl implements HeaderBarConfigService +{ + getBreadcrumbsConfig(): Promise { + return { + autoHide: true, + omitRoot: false, + pendingItemLabel: '...', + rightRenderers: [( + containerElement: HTMLElement, + nodeItems: NodeItem[], + clickHandler, + ) => { + // Example: Adding a custom 'Live' status badge to the right side + const badge = document.createElement('span'); + badge.innerText = 'LIVE'; + badge.style.background = 'green'; + badge.style.color = 'white'; + badge.style.padding = '2px 6px'; + badge.style.borderRadius = '4px'; + containerElement.appendChild(badge); + }], + leftRenderers: [( + containerElement: HTMLElement, + nodeItems: NodeItem[], + clickHandler, + ) => { + // ... renderer implementation + }], + }; + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { HeaderBarConfigServiceImpl } from './header-bar-config.service'; + +const portalOptions: PortalOptions = { + // Reference the service class here. The library handles instantiation. + headerBarConfigService: HeaderBarConfigServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); + +``` diff --git a/documentation/extended-guide/frontend-configuration/index.md b/documentation/extended-guide/frontend-configuration/index.md new file mode 100644 index 00000000..5dab1d0e --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/index.md @@ -0,0 +1,10 @@ +# Frontend configuration + +The frontend module, available in **openmfp/portal-ui-lib** is an Angular library that +comes with a `providePortal` function that is designed to accept a set of configurations, +allowing you to customize the portal to your needs. + +In this guide, you’ll learn: + +* How to set up the required and optional services +* How they work together diff --git a/documentation/extended-guide/frontend-configuration/luigi-auth-callbacks-settings.md b/documentation/extended-guide/frontend-configuration/luigi-auth-callbacks-settings.md new file mode 100644 index 00000000..533d4050 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/luigi-auth-callbacks-settings.md @@ -0,0 +1,105 @@ +# Handling Authentication Events + +The `luigiAuthEventsCallbacksService` option allows you to define a service that intercepts +[Luigi authorization events](https://docs.luigi-project.io/docs/authorization-events). +This is useful for performing side effects—such as clearing local storage, +redirecting users, or logging telemetry—when the authentication state changes. + +## LuigiAuthEventsCallbacksService Interface + +```ts +export interface LuigiAuthEventsCallbacksService { + /** + * Triggered when a user successfully authenticates. + */ + onAuthSuccessful: (settings: any, authData: any) => void; + + /** + * Triggered when an error occurs during the authentication process. + */ + onAuthError: (settings: any, err: any) => void; + + /** + * Triggered when the current authentication session has expired. + */ + onAuthExpired: (settings: any) => void; + + /** + * Triggered when the user initiates a logout action. + */ + onLogout: (settings: any) => void; + + /** + * Triggered shortly before the authentication session is set to expire. + */ + onAuthExpireSoon: (settings: any) => void; + + /** + * Triggered when there is an error in the authentication configuration. + */ + onAuthConfigError: (settings: any, err: any) => void; +} +``` + +## Provide your implemented service + +```ts +import { LuigiAuthEventsCallbacksService } from '@openmfp/portal-ui-lib'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class LuigiAuthEventsCallbacksServiceImpl + implements LuigiAuthEventsCallbacksService { + + onAuthSuccessful = (settings: any, authData: any) => { + console.log('User successfully authenticated.', authData); + // Logic: Initialize user-specific session data + }; + + onAuthError = (settings: any, err: any) => { + console.error('Authentication failed:', err); + }; + + onAuthExpired = (settings: any) => { + console.warn('Authentication session expired.'); + // Logic: Redirect to login or show session expired modal + }; + + onLogout = (settings: any) => { + console.log('User initiated logout.'); + // Logic: Clear sensitive data from local storage + }; + + onAuthExpireSoon = (settings: any) => { + console.warn('Session will expire soon. Consider refreshing.'); + // Logic: Show a "Extend Session" notification to the user + }; + + onAuthConfigError = (settings: any, err: any) => { + console.error('Critical Auth Config Error:', err); + }; +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { LuigiAuthEventsCallbacksServiceImpl } from './auth-events-callbacks.service'; + +const portalOptions: PortalOptions = { + // Provide the service class reference + luigiAuthEventsCallbacksService: LuigiAuthEventsCallbacksServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); + +``` diff --git a/documentation/extended-guide/frontend-configuration/luigi-global-context.md b/documentation/extended-guide/frontend-configuration/luigi-global-context.md new file mode 100644 index 00000000..bf0b27c8 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/luigi-global-context.md @@ -0,0 +1,82 @@ +# Extending Luigi Global Context + +The `luigiExtendedGlobalContextConfigService` option allows you to inject custom data into the +[Luigi's global context](https://docs.luigi-project.io/docs/navigation-parameters-reference?section=globalcontext) . +This data becomes available to all micro-frontends integrated into the portal. + + +## Default Context Data + +By default, the Portal UI Library automatically populates the global context with the following information: + +```json +{ + "portalContext": { ... }, // data retrieved from the portal backend with /rest/config call + "portalBaseUrl": "[https://your-portal.com](https://your-portal.com)", + "userId": "unique-user-id", + "userEmail": "user@example.com", + "token": "id-token-string" +} +``` + +## LuigiExtendedGlobalContextConfigService Interface + +```ts +export interface LuigiExtendedGlobalContextConfigService { + createLuigiExtendedGlobalContext(): Promise; +} +``` + +## Provide your implemented service + +Your implementation should define the additional properties you want to share across your micro-frontend ecosystem, +such as environment flags or global configuration strings. + +```ts +import { + LuigiExtendedGlobalContextConfigService +} from '@openmfp/portal-ui-lib'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class LuigiExtendedGlobalContextConfigServiceImpl + implements LuigiExtendedGlobalContextConfigService { + + /** + * Defines custom data to be appended to the default Luigi global context. + */ + async createLuigiExtendedGlobalContext(): Promise { + // You could also fetch data from an API here + return { + isLocal: true, + analyticsConfig: 'production-v1', + themeVariant: 'dark' + }; + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { + LuigiExtendedGlobalContextConfigServiceImpl +} from './global-context.service'; + +const portalOptions: PortalOptions = { + // Reference the service class here. The library handles instantiation. + luigiExtendedGlobalContextConfigService: LuigiExtendedGlobalContextConfigServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + diff --git a/documentation/extended-guide/frontend-configuration/luigi-node-context.md b/documentation/extended-guide/frontend-configuration/luigi-node-context.md new file mode 100644 index 00000000..bb18e681 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/luigi-node-context.md @@ -0,0 +1,87 @@ +# Extending Node Context + +The `nodeContextProcessingService` option allows you to inject custom data into the +[Luigi's global context](https://docs.luigi-project.io/docs/navigation-parameters-reference?section=globalcontext) of a node while navigating to that node. +This data becomes available to the navigated micro-frontend and all it's children. + +## LuigiExtendedGlobalContextConfigService Interface + +```ts +export interface NodeContextProcessingService { + /** + * @param entityId The ID of the current entity. + * @param entityNode The LuigiNode node being navigated to. + * @param ctx The current context object calculated by Luigi. + */ + processNodeContext( + entityId: string | undefined, + entityNode: LuigiNode, + ctx: NodeContext, + ): Promise; +} +``` + +## Provide your implemented service + +In your implementation, you can fetch data from a backend service and append it to the entityNode.context. +This is particularly useful for adding labels or metadata that are not available in the static configuration. + +```ts +import { PortalNodeContext } from '../models/luigi-context'; +import { PortalLuigiNode } from '../models/luigi-node'; +import { Injectable, inject } from '@angular/core'; +import { NodeContextProcessingService } from '@openmfp/portal-ui-lib'; +import { ResourceService } from '@platform-mesh/portal-ui-lib/services'; + +@Injectable({ + providedIn: 'root', +}) +export class NodeContextProcessingServiceImpl implements NodeContextProcessingService { + private resourceService = inject(ResourceService); + + public async processNodeContext( + entityId: string, + entityNode: PortalLuigiNode, + ctx: PortalNodeContext, + ) { + + try { + // Fetch data asynchronously (e.g., from a Kubernetes resource or API) + const entity = this.resourceService.read(entityId); + + // Inject the retrieved data into the node's context + entityNode.context.entityDisplayName = entity?.metadata?.name || 'Unknown'; + } catch (e) { + console.error(`Not able to read entity ${entityId}`); + // Optionally throw or handle error to prevent/redirect navigation + throw e; + } + } +} + +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { + NodeContextProcessingServiceImpl +} from './node-context.service'; + +const portalOptions: PortalOptions = { + // Reference the service class here. The library handles instantiation. + nodeContextProcessingService: NodeContextProcessingServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + diff --git a/documentation/extended-guide/frontend-configuration/message-listeners.md b/documentation/extended-guide/frontend-configuration/message-listeners.md new file mode 100644 index 00000000..1f28f073 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/message-listeners.md @@ -0,0 +1,103 @@ +# Custom Message Listeners + +To have a possibility to react to custom messages sent from micro frontends the `customMessageListeners` option allows +you to define one or more listeners. This provides a robust mechanism for application-wide communication. + + +## CustomMessageListener Interface + +```ts +export interface CustomMessageListener { + /** + * The custom message id the listener is registered for. + */ + messageId(): string; + + /** + * The callback to be executed when the custom message is send by Luigi. + * + * @param customMessage The message object, see also {@link https://docs.luigi-project.io/docs/luigi-client-api?section=sendcustommessage} + * @param mfObject The micro frontend object, see also {@link https://docs.luigi-project.io/docs/luigi-core-api?section=getmicrofrontends} + * @param mfNodesObject The nodes object of the micro frontend, see also {@link https://docs.luigi-project.io/docs/navigation-parameters-reference?section=node-parameters} + */ + onCustomMessageReceived( + customMessage: any, + mfObject: any, + mfNodesObject: any, + ): void; +} +``` + +## Provide your implemented listener + +```ts +import { CustomMessageListener } from '@openmfp/portal-ui-lib'; + +export class CustomMessageListenerImpl + implements CustomMessageListener +{ + messageId(): string { + return 'unique.message.id'; + } + + onCustomMessageReceived( + customMessage: any, + mfObject: any, + mfNodesObject: any, + ): void { + const data = customMessage.data; + console.log(`Custom Message Received: ${customMessage.id}`, data); + // ... logic to be executed + } +} +``` + +## Sending a custom message + +Custom messages are sent from any part of your application to the portal. +A custom message is sent by using Luigi client method `sendCustomMessage({ id: 'unique.message.id'});` +method (see also the following example). + +```ts +import { Injectable } from '@angular/core'; +import { sendCustomMessage } from '@luigi-project/client/luigi-client'; + +@Injectable({ providedIn: 'root' }) +export class NotifyUniqueService { + public sendUniqueCustomMessage() { + sendCustomMessage({ + id: 'unique.message.id', + origin: 'MyApp', + entity: 'myEntity', + data: { user: 'Test User', message: 'Hello World!' }, + }); + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { CustomMessageListenerImpl } from './custom-message-listener.service'; +import { CustomMessageListenerImpl2 } from './other-listener.service'; + +const portalOptions: PortalOptions = { + // Provide an array of listener classes + customMessageListeners: [ + CustomMessageListenerImpl, + CustomMessageListenerImpl2, + ], + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + diff --git a/documentation/extended-guide/frontend-configuration/node-change-hook-settings.md b/documentation/extended-guide/frontend-configuration/node-change-hook-settings.md new file mode 100644 index 00000000..fcfb1711 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/node-change-hook-settings.md @@ -0,0 +1,88 @@ +# Customizing Luigi Node Change Hooks + +The `nodeChangeHookConfigService` option allows you to execute custom logic whenever a +navigation event occurs within the [Luigi shell](https://docs.luigi-project.io/docs/navigation-parameters-reference?section=nodechangehook). +This is a powerful tool for tracking user behavior, +updating page titles dynamically, or managing side effects that depend on the transition between +different navigation nodes. + +## NodeChangeHookConfigService Interface + +The `NodeChangeHookConfigService` interface defines a single method +that is triggered after a node change has been processed by Luigi. + +```ts +import { LuigiNode } from '@openmfp/portal-ui-lib'; + +export interface NodeChangeHookConfigService { + /** + * @param prevNode The LuigiNode the user is navigating away from. + * @param nextNode The LuigiNode the user is currently navigating to. + */ + nodeChangeHook(prevNode: LuigiNode, nextNode: LuigiNode): void; +} +``` + +## Provide your implemented service + +In your implementation, you can react to specific node changes. For instance, +you might want to send an analytics event or reset a global state when a user moves to a +specific part of the application. + +```ts +import { Injectable } from '@angular/core'; +import { NodeChangeHookConfigService, LuigiNode } from '@openmfp/portal-ui-lib'; + +@Injectable({ + providedIn: 'root', +}) +export class CustomNodeChangeHookService implements NodeChangeHookConfigService { + + /** + * Responds to navigation changes across the portal. + */ + nodeChangeHook(prevNode: LuigiNode, nextNode: LuigiNode): void { + const prevPath = prevNode?.pathSegment || 'root'; + const nextPath = nextNode?.pathSegment || 'root'; + + console.log(`Navigation transition: ${prevPath} -> ${nextPath}`); + + // Example: Update the document title based on the node label + if (nextNode?.label) { + document.title = `Portal - ${nextNode.label}`; + } + + // Example: Trigger analytics for a specific node + if (nextNode?.viewUrl?.includes('dashboard')) { + this.sendAnalytics('view_dashboard'); + } + } + + private sendAnalytics(eventName: string): void { + // Your analytics logic here + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { CustomNodeChangeHookService } from './node-change-hook.service'; + +const portalOptions: PortalOptions = { + // Reference the service class here. The library handles instantiation. + nodeChangeHookConfigService: CustomNodeChangeHookService, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + diff --git a/documentation/extended-guide/frontend-configuration/node-processing.md b/documentation/extended-guide/frontend-configuration/node-processing.md new file mode 100644 index 00000000..8f9fe8ff --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/node-processing.md @@ -0,0 +1,85 @@ +# Customizing Node Processing + +The `customNodeProcessingService` option allows you to intercept and modify individual +Luigi Nodes dynamically before they are rendered in the navigation tree. +This is specifically useful for implementing custom access policies, dynamic labeling based on context, +or modifying node properties (like icons or descriptions) on the fly. + +## CustomNodeProcessingService Interface + +The `CustomNodeProcessingService` interface requires a single asynchronous method. +It receives the current navigation context and the specific node being processed. + +```ts +import { Context } from '@luigi-project/client'; +import { LuigiNode } from '@openmfp/portal-ui-lib'; + +export interface CustomNodeProcessingService { + /** + * @param ctx The current Luigi context. + * @param node The node currently being processed by the library. + * @returns A Promise resolving to the modified (or original) LuigiNode. + */ + processNode(ctx: Context, node: LuigiNode): Promise; +} +``` + +## Provide your implemented service + +In this example, the service checks for a custom property requiredPermission in the node context. +If the user doesn't have that permission (based on the global context), the node is hidden. +It also demonstrates how to dynamically update a node's label. + +```ts +import { Injectable } from '@angular/core'; +import { Context } from '@luigi-project/client'; +import { CustomNodeProcessingService, LuigiNode } from '@openmfp/portal-ui-lib'; + +@Injectable({ + providedIn: 'root', +}) +export class CustomNodeProcessingServiceImpl implements CustomNodeProcessingService { + + public async processNode(ctx: Context, node: LuigiNode): Promise { + // 1. Implementation of a custom access policy + if (node.context?.requiredPermission) { + const userPermissions = ctx.userPermissions || []; + if (!userPermissions.includes(node.context.requiredPermission)) { + node.hideFromNav = true; + } + } + + // 2. Dynamic modification of node properties + if (node.pathSegment === 'project-details' && ctx.projectName) { + node.label = `Project: ${ctx.projectName}`; + } + + // Return the potentially modified node + return node; + } +} + +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { CustomNodeProcessingServiceImpl } from './custom-node-processing.service'; + +const portalOptions: PortalOptions = { + // Reference the service class here. + customNodeProcessingService: CustomNodeProcessingServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + diff --git a/documentation/extended-guide/frontend-configuration/routing-settings.md b/documentation/extended-guide/frontend-configuration/routing-settings.md new file mode 100644 index 00000000..33877a9a --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/routing-settings.md @@ -0,0 +1,84 @@ +# Customizing Luigi Routing Configuration + +To customize the routing behavior within Luigi (e.g., hash usage, modal URL parameters, or 404 error handling), +you must implement the `RoutingConfigService` interface and provide your custom service during +the application bootstrap. This service allows you to override any default +[Luigis routing settings](https://docs.luigi-project.io/docs/navigation-parameters-reference?section=routing-parameters) + +## RoutingConfigService Interface + +The `RoutingConfigService` interface defines two synchronous methods used to configure +routing at different stages of the Luigi lifecycle: + +```ts +export interface RoutingConfigService { + getInitialRoutingConfig(): any; + getRoutingConfig(): any; +} +``` + +## Provide your implemented service + +```ts +import { Injectable, inject } from '@angular/core'; +import { RoutingConfigService } from '@openmfp/portal-ui-lib'; + +@Injectable() +export class CustomRoutingConfigService implements RoutingConfigService { + + /** + * Defines the initial Luigi routing configuration. + */ + getInitialRoutingConfig(): any { + return { + useHashRouting: false, + showModalPathInUrl: true, + modalPathParam: 'modalPathParamDisabled', + }; + } + + /** + * Defines the routing configuration after initialization. + */ + getRoutingConfig(): any { + return { + useHashRouting: true, + pageNotFoundHandler: ( + notFoundPath: string, + isAnyPathMatched: boolean, + ) => { + return { + redirectTo: 'handle-error/404', + keepURL: true, + }; + }, + }; + } +} +``` + +* The `getInitialRoutingConfig()` is invoked while constructing the initial Luigi configuration object. +* The `getRoutingConfig()` is invoked during the Luigi lifecycle hook luigiAfterInit. This configuration adds additional setup to the root of the Luigi configuration object. + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { CustomRoutingConfigService } from './custom-routing-config.service'; // Import your new service + +const portalOptions: PortalOptions = { + // Reference the service class here. The library will instantiate it. + routingConfigService: CustomRoutingConfigService, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); +``` + diff --git a/documentation/extended-guide/frontend-configuration/user-profile-settings.md b/documentation/extended-guide/frontend-configuration/user-profile-settings.md new file mode 100644 index 00000000..1ddab2e9 --- /dev/null +++ b/documentation/extended-guide/frontend-configuration/user-profile-settings.md @@ -0,0 +1,79 @@ +# Customizing the User Profile + +The `userProfileConfigService` option allows you to define and manage the content of the +[Luigi user profile](https://docs.luigi-project.io/docs/navigation-advanced?section=profile). +This dropdown, typically located in the top-right corner of the shell, +contains links to user settings, profile overviews, and the logout functionality. + +## UserProfileConfigService Interface + +```ts +export interface UserProfileConfigService { + /** + * Returns a Promise resolving to a UserProfile object + * defining the logout behavior and additional menu items. + */ + getProfile(): Promise; +} +``` + +## Provide your implemented service + +```ts +import { UserProfileConfigService, UserProfile } from '@openmfp/portal-ui-lib'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class UserProfileConfigServiceImpl implements UserProfileConfigService { + + /** + * Generates the profile menu configuration. + */ + async getProfile(): Promise { + return { + // Configure the sign-out button + logout: { + label: 'Sign out', + icon: 'log', + // Optional: custom logout handling logic can be defined here + }, + // Define additional navigation items in the profile dropdown + items: [ + { + label: 'My Overview', + icon: 'customer-view', + link: '/users/overview', + }, + { + label: 'Account Settings', + icon: 'settings', + link: '/users/settings', + } + ] + }; + } +} +``` + +## Registering the service during application bootstrap + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { + PortalComponent, + PortalOptions, + providePortal, +} from '@openmfp/portal-ui-lib'; +import { UserProfileConfigServiceImpl } from './user-profile.service'; + +const portalOptions: PortalOptions = { + // Reference the service class here. The library handles instantiation. + userProfileConfigService: UserProfileConfigServiceImpl, + // ... other portal options +}; + +bootstrapApplication(PortalComponent, { + providers: [providePortal(portalOptions)], +}).catch((err) => console.error(err)); + +``` diff --git a/documentation/extended-guide/register-microfrontend.md b/documentation/extended-guide/register-microfrontend.md deleted file mode 100644 index 9b535b37..00000000 --- a/documentation/extended-guide/register-microfrontend.md +++ /dev/null @@ -1,105 +0,0 @@ -# Registering a Microfrontend in OpenMFP - -You can register a micro frontend in the OpenMFP Portal to integrate it permanently into your portal. -This guide walks you through the process of registering a micro frontend. - -## Steps to Register a Microfrontend - -### 1. Create a ContentConfiguration - -To register a micro frontend in OpenMFP, you need to create a `ContentConfiguration`. This is a Kubernetes Custom Resource (CRD) managed by the `openmfp-extension-manager-operator`. -Once deployed, the operator reconciles the resource to reflect the defined configuration. - -The `ContentConfiguration` can be defined using either `remoteConfiguration` or `inlineConfiguration`: - -- **Remote configuration**: References an externally hosted JSON or YAML file. The specified URL must be accessible by the cluster to ensure proper retrieval and application of the configuration. -- **Inline configuration**: Embeds the JSON or YAML content directly in the `ContentConfiguration` resource. - -Before proceeding, create a file named `content-configuration.yaml` and define your desired configuration as specified in the [Content Configuration](/documentation/extended-guide/content-configuration) section. You can also follow the next examples. - -#### Example: Remote Configuration -```yaml -apiVersion: core.openmfp.io/v1alpha1 -kind: ContentConfiguration -metadata: - name: my-microfrontend - namespace: openmfp-system - labels: - portal.openmfp.org/entity: global -spec: - remoteConfiguration: - url: https://url.hosting.my.microfrontend/assets/content-configuration.json - contentType: json -``` - -#### Example: Inline Configuration -```yaml -apiVersion: core.openmfp.io/v1alpha1 -kind: ContentConfiguration -metadata: - name: my-microfrontend - namespace: openmfp-system - labels: - portal.openmfp.org/entity: global -spec: - inlineConfiguration: - content: |- - { - "name": "my-microfrontend", - "url": "https://url.hosting.my.microfrontend", - "luigiConfigFragment": { - "data": { - "nodes": [ - { - "pathSegment": "ur-path-segment-inline", - "label": "My Microfrontend", - "entityType": "global", - "hideFromNav": false, - "urlSuffix": "/index.html", - "icon": "folder", - "loadingIndicator": { - "enabled": false - } - } - ] - } - } - } - contentType: json -``` - -Ensure that the label `portal.openmfp.org/entity` is present. - -### 2. Apply the Configuration - -Once you have created the YAML file, apply it to the Kubernetes cluster using the following command: - -```sh -kubectl apply -f content-configuration.yaml --context kind-openmfp -``` - -### 3. Edit an Existing Configuration - -#### Remote Configuration -- Changes made to the remote `content-configuration.json` are automatically reflected in the OpenMFP Portal within five minutes. -- To force immediate updates, manually edit the `ContentConfiguration` Custom Resource in the cluster so that the operator reconciles the resource. - -#### Inline Configuration -- Changes made in the inline configuration are immediately reflected in the OpenMFP Portal after editing the `ContentConfiguration` Custom Resource in the cluster. - -To update an existing `ContentConfiguration`, edit it directly in the cluster: - -```sh -kubectl edit contentconfiguration my-microfrontend --namespace openmfp-system --context kind-openmfp -``` -Alternatively, modify the YAML file and reapply it: - -```sh -kubectl apply -f content-configuration.yaml --context kind-openmfp -``` - -### 4. View the Microfrontend in the OpenMFP Portal - -After successfully applying the configuration, your micro frontend should be visible in the OpenMFP portal. - -![My Microfrontend in the OpenMFP Portal](/my-microfrontend.png)