@bluealba-public/pae-ui-react-core
v1.2.1
Published
React utilities for PAE
Downloads
513
Readme
PAE - UI React Core
Core PAE library for PAE microfrontend applications. It exposes PAE functionality in the form of react elements such as hooks, components, etc. which provide cross-cutting concerns such as Authentication (Impersonation), Authorization, Service Interaction, Monitoring, etc.
Usage
Initialize your Microfrontend
A PAE microfrontend must be created by calling the initiliazeMicroFrontend()
function (not although you could directly create a SingleSPA application
that will tied you to SPA and you won't be able to use all the functionality of this PAE library).
import React from "react";
import ReactDOMClient from "react-dom/client";
import { initializeMicroFrontend } from "@bluealba-public/pae-ui-react-core";
import Root from "./root.component";
export const { bootstrap, mount, unmount } = initializeMicroFrontend({
React,
ReactDOMClient,
rootComponent: Root,
errorBoundary(err) {
console.error(err);
return (
<div>
<h1>Something went wrong</h1>
<p>{err.message}</p>
</div>
);
}
});
The most important part here is that you specify your rootComponent
of your app and a way to handle errors locally.
Utility Functions
navigateTo() utility function
Use this function to navigate to a different url. This function is a wrapper around the single-spa
navigateToURL
function
import { navigateTo } from '@bluealba-public/pae-ui-react-core';
navigateTo('/path/to/another/module');
Hooks
PAE provides the following hooks to access data and interact with PAE services.
useAuth
Retrieve the current user information as well as a method to check authorization access.
import { useAuth } from '@bluealba-public/pae-ui-react-core';
const TestComponent = () => {
const { authUser, hasAccess } = useAuth();
return (
<div>
<h1>{authUser.displayName}</h1> {/* e.g: John Doe */}
<p>{authUser.username}</p> {/* e.g: johndoe */}
<p>{authUser.familyName}</p> {/* e.g: Doe */}
<p>{authUser.givenName}</p> {/* e.g: John */}
<p>{authUser.initials}</p> {/* e.g: JD */}
<p>{hasAccess('operation1', 'operation2')}</p>
</div>
);
};
useCatalog
Use this hook to get access to the current ecosystem applications and modules catalog
import { useCatalog } from '@bluealba-public/pae-ui-react-core';
const TestComponent = () => {
const { catalog } = useCatalog();
return (
<div>
{catalog.map((item) => (
<div key={item.name}>
<h1>{item.name}</h1>
<p>{item.version}</p>
</div>
))}
</div>
);
};
useServiceInvoker
Use this service to make a request to a PAE microservice. This way you don't need to know under which path it is exposed
by the system (gateway). And you just need to know the service/module full name.
It returns a function that is fully compatible with the fetch
function
const invoker = useServiceInvoker("@acme/my-wheather-service");
return (
<button onClick={() => invoker('/get-temperature', { method: 'GET' })}>Load</button>
)
Internally this hook uses the catalog to resolve the service full URL (there is also a useServiceBaseUrl
hook)
useTrackEvent
Use this service to track Application Usage Events. This is the frontend API to PAE Habits Service.
const trackEvent = useTrackEvent();
const trackClickEvent = useCallback(async e => {
await trackEvent("button-click-app2", {
buttonText: e.target.innerText
});
},
[trackEvent]
);
return (
<button onClick={trackClickEvent}>Click Me</button>
)
The trackEvent()
function receives the name of the event and a custom payload that can be anything you want.
PAE will automatically store this event with contextual information such as: the current User, the Module that triggered
this event and its version and the associated Application (if the module has one), together with timestamps.
Alternatively you can also use useTrackEvents()
(plural) hook to publish more than one event in a single call.
Authorization
In order to render a piece of JSX conditionally based on authorization there are two ways to do this.
- using the
useAuth()
hook: which is more "manual" but powerful - using the
<Authorized>
component
useAuth()
With this hook you get the current user and a utility function hasAccess()
const MyComponent = () => {
const { authUser, hasAccess } = useAuth();
return (
<div>
{hasAccess('operation1', 'operation2') && (
<div>access granted</div>
)}
</div>
)
}
<Authorized>
component (recommended)
This component only renders the children content if the user has the specified operations
const MyComponent = () => {
return (
<Authorized operations={['create-entity', 'update-entity']}>
<SectionContent>
<p>You have access to this section</p>
</SectionContent>
</Authorized>
)
}
If it has no access then nothing is rendered.
There's also an optional property forbiddenContent
with which you can specify the content to be displayed in case the user has no grant on the operations.
This prop is overloaded and you can pass different content.
- The name of a React component
- A render function
- A straight JSX content
For example a render function
// passing a React component
<Authorized operations={['create-entity', 'update-entity']} forbiddenContent={ForbiddenComponent}>
<SectionContent>
<p>You have access to this section</p>
</SectionContent>
</Authorized>
// passing a render prop function
<Authorized operations={['create-entity', 'update-entity']} forbiddenContent={({ operations }) => <ForbiddenComponent operations={operations} />}>
<SectionContent>
<p>You have access to this section</p>
</SectionContent>
</Authorized>
// passing content directly
<Authorized operations={['create-entity', 'update-entity']} forbiddenContent={<div>Section not allowed</div>}>
<SectionContent>
<p>You have access to this section</p>
</SectionContent>
</Authorized>
Extension Points
They allow UI modules to declare points such as React components that can be later replaced by another value/component from another UI module dynamically.
Declaring an extension point
There are two ways for a MF to declare an extension point that can be later extended by other MFs. Internally they both the the same thing, it is just a matter of a different flavor of API.
<ExtensionPoint>
component: useful to declare an extension point inline within a JSXextensionPoint()
HOC (high-order component): useful to make a whole component replaceable as an extension point.
Declaring an extension point inline with ExtensionPoint component
In any component’s JSX you can declare an extension point, like a “slot” to be extended by other MFs.
const MyComponent = () => {
return (
<div>
Hello <ExtensionPoint name="Greeting" />
</div>
)
}
This will create an extension point that is scoped to the current module with the given name. For example my-module::Greeting
You can include children elements that will be the fallback/default content in case nobody extends this extension point
const MyComponent = () => {
return (
<div>
Hello <ExtensionPoint name="Greeting"> World </ExtensionPoint>
</div>
)
}
Declaring a component as an extension point with the HOC
There’s another common case where you want a whole component to be “replaceable” completely as an extension point. In this case you can use the extensionPoint(component)
HOC as a shortcut to void creating a wrapper component just for this.
For example the pae-shell-ui
(or any MF) declares a React component to be an “extension point” by simply using a new pae-ui-react-core
HOC wrapping the component that is the menu
const MenuApplicationSelector: React.FC = () => {
// regular react component
}
// the displayName becomes the extension point name by inference (it can be set manual optionally in the hoc call)
MenuApplicationSelector.displayName = 'ApplicationSelector';
// here we are wrapping the component using the HOC which makes it an extension point
export default extensionPoint(MenuApplicationSelector);
If no-one is extending the point the the component will be rendered. If someone extends it then this component won’t be mounted at all, and it will be replaced by the extending component.
Extending an Extension Point
Then another MF can extend that point in two different ways:
ExtendExtensionPoint
: Using a built-in component rendering the content inlineuseExtendExtensionPoint
: using a hook which allows more programmatic control
We will see both options below:
Using the ExtendExtensionPoint component
With a React Component provided by pae-ui-react-core
// somewhere in your MF DOM
<ExtendExtensionPoint module="@bluealba/pae-shell-ui" point="ApplicationSelector">
Hello <strong>World</strong> From an Extension!
</ExtendExtensionPoint>
Using the useExtendExtensionPoint hook
Or more "manually" using the useExtendExtensionPoint hook
const Root = () => {
// register the extension
useExtendExtensionPoint('@bluealba/pae-shell-ui', 'ApplicationSelector', CustomApplicationMenu)
return <div> your app content here </div>
}
In both cases the extension follows the regular React lifecycle ! Meaning that when the component that called the hook/included the extension component gets unmounted, then the extension point gets un-registered dynamically. Then this means that components are extended / disengaged dynamically.
Library Development
This section is for PAE platform developers or anyone that wants to make changes to this repository. This Library is one of the "client facing" API for PAE. Meaning that it is the API that exposes all PAE behavior to microfrontends for different customers and projects. Because of this we must be very careful with changes in terms of backward compatibility. This is the first layer of integration. Some projects might be using different versions of PAE and this library.
Therefore we should try to reduce the number of non-backwards compatible changes !!
Development Process and Releasing
To make modifications and have them automatically picked while working locally you should run
npm run build-and-publish:watch
Just make sure that you have the pae-orchestrator-service
cloned as a sibling folder to this repo.
The following content describes what happens under the hood. The cycle goes like this:
- Create feature branches to work in a feature
- Eventually merge them into
develop
branch. This will build but not publish any package - Once you reached a point where you want to publish a new release. Bump the version to the target version you want to publish and create a PR to
main
- Once merged into main the pipeline will automatically build and publish the version to NPMjs public repository.
Now there are 2 more steps to follow in order to start using this published version (next sections)
Update PAE Orchestrator
In the PAE architecture the orchestrator is the one that automatically includes this library in runtime for all applications. It has its own locally cached bundle js of this library. To update it you must
- Edit the
build/libs/build-libs.mjs
file inpae-orchestrator-service
to update the "shared-libraries" entry with the version you just published - Run
npm run build:libs
in the orchestrator so that it fetches this new version and caches it - Merge to develop to create a new build/image of the orchestrator
- Apply it to any environment / product you want
Projects Microfrontends
Every microfrontend includes this library as a devDependency
so that Typescript can use it for type checking.
Then once built this is not included, because the orchestrator includes it in runtime (think of it as a dynamically
linked library).
Nevertheless this means that in order to catch errors and be consistent each microfrontend dependency must match
the same version of this lib as the orchestrator has for its specific product.
So once you release a new version of this lib and update the orchestrator to use it. When you bump the orchestrator version for a particular product, you should be going through all your microfrontends and update the devDependency to match the released version.