Justice Admin Portal Extension Website is a web application that extends Admin Portal functionality, mainly to fulfill client's needs that not included in Admin Portal. Please also check this Development Guideline
We extend Admin Portal by loading this Extension website through an iframe in Admin Portal page.
- Admin Portal get manifest provided by Extension that contains page information
- Use that manifest to generate menus in the sidebar
- Load Extension website through iframe when user open the menu
Admin Portal and Extension website shares token through browser's cookies.
Project need to be built first in order to make it works:
make build
To start the development environment, run the following command:
make run
The project will be served on localhost:3003
- Docker with support for Linux containers
- GNU Make
- Internet connection
- See Build Requirements
- Docker Compose
- IDE (we like WebStorm)
- To add / change / remove environment variables:
- Change this (README.md) document
- Add environment variables that is read on runtime in deployment/k8s/deployment.yaml, config/env.js, and docker-compose.yaml
- Add environment variables, both that is read on runtime or build time on makefile
- Add conditions (if necessary) on scripts/env-guard
Environment variables that will be read on runtime :
Environment Variables | Optional | Description |
---|---|---|
JUSTICE_BASE_URL | No | The base url of the API which the Admin Portal will call |
JUSTICE_BASE_PATH | No | The base path of extension |
JUSTICE_PUBLISHER_NAMESPACE | Yes | The publisher namespace |
JUSTICE_ADMIN_BEARER_TOKEN_DEVMODE | Yes | For dev purpose only, the bearer token of currently logged in user |
src
├── api
│ └── backend-service-name
│ ├── models
| | └── client.ts (where we have model)
| └── client.ts (where we place api call utils)
├── app-messages (initialize global events here)
├── app-states (place global states here)
├── app-tasks (place tasks to run on App's didMount before render any UI)
│ ├── index.ts (register tasks here)
│ └── initATask.tsx
├── packages (create custom modules here, please follow the module example for the folder structure)
│ └── Module
│ └── SubModule
├── routes
│ └── PrivateRoutes.tsx (register submodule's route here)
Please check this doc
To make our custom works shown in Admin Portal, we need to set up modules and submodules.
- Module: The high level service of Admin Portal e.g. Analytics, Users Management, E-Commerce, etc
- SubModule: The specific features/packages inside Module e.g. Users in Users Management, Stores in E-Commerce, etc
To create new module, please follow these steps:
- Create new folder inside packages
- Add new
module.json
file insidemodule
folder
{
"id": "example", // unique module id
"title": "module.example.title", // translation key for module title
"icon": "icon-ab-sidebar-users" // icon that will be shown in AP sidebar
}
To create new submodule, please follow these steps:
- Inside a
module
, create new folder - Add
submodule.json
file insidesubmodule
folder
{
"id": "example-submodule", // unique module id
"title": "module.example.submodule.example.title", // translation key for module title
"link": "/namespace/{namespace}/example-submodule", // link to access the submodule page
"permission": {
"resource": "ADMIN:NAMESPACE:{namespace}:USER:*", // permission to access submodule
"action": 2 // action to access from Admin Portal sidebar
},
"allowedNamespaces": ["accelbyte"] // optional, allowed namespace to access submodule
}
- Create a React component just like usual Admin Portal packages
URL pattern:
http://localhost:3003/admin-extension/namespaces/{namespace}/{moduleId}
namespace
: current namespace you are currently working onmoduleId
: Module/SubModule's id. You can get this value fromid
inmodule.json
/submodule.json
e.g
http://localhost:3003/admin-extension/namespaces/accelbyte/example
http://localhost:3003/admin-extension/namespaces/accelbyte/example-submodule
Sometimes we want a feature to only available on specific namespace, we can use allowedNamespaces
as a rule to hide/show the feature. Please refer to the table below on how to apply the rule.
Value | Output |
---|---|
allowedNamespaces: undefined | feature will appear in all namespace |
allowedNamespaces: [] | feature will appear in all namespace |
allowedNamespaces: ["accelbyte"] | feature will appear ONLY in "accelbyte" namespace |
allowedNamespaces: ["accelbyte", "gamenamespace1", "gamenamespace2"] | feature will appear ONLY in "accelbyte", "gamenamespace1" and "gamenamespace2" namespace |
Import module/submodule's route component and register in src/routes/PrivateRoutes
We have common components that match with Admin Portal's design system.
import { Card, Page } from "justice-ui-library";
const SomeComponent = () => (
<Page title="User">
<Card cardTitle={"Current User"} noHorizontalMargin>
<ContentHere />
</Card>
</Page>
);
We do network call with axios and runtime checking with io-ts. In summary this is the step on how to do network call.
- Create response model
- Create network call utility
- Do network call
- Handle success and error after network call
We use runtime checking with io-ts
to make sure we get expected response data. Please check io-ts for complete guidelines.
export const Item = ioTs.intersection([
ioTs.type({
itemId: string,
}),
ioTs.partial({
description: ioTs.string,
}),
]);
export type Item = ioTs.TypeOf<typeof Item>;
export class ItemDecodeError extends DecodeError {}
We mainly use guardNetworkCall
to guard a network call. This function handles network call and do runtime checking.
guardNetworkCall
has 4 parameters:
- Network call callback, this callback is expected to return a
AxiosResponse
- Response model, the function will do runtime check to validate this model and the response data
- DecodeError class, this class will be thrown when response data and response model is mismatch
- transform error callback, change your error with this callback
guardNetworkCall
will always has this return object
const { response, error } = await guardNetworkCall(...params);
response
will be null when an error thrown inside guardNetworkCall
and vice versa. Any error thrown will be in error
(network call error like 400, runtime check error).
import * as ioTs from "io-ts";
import { guardNetworkCall } from "src/api/networkCallTypeguard";
export function fetchSomething(network: Network, namespace: string) {
return guardNetworkCall(
() => network.get(`/endpoint`),
ioTs.array(Item),
ItemDecodeError,
(error) => error
);
}
fetchSomething(networkManager.withCredentials(), namespace)
.then((result) => {
if (!!result.error) throw result.error;
// do your things here
console.log(result.response.data);
})
.catch((error) => {
// handle error here
});
When we have successfully do something or error happens during a process, we might want to show a toast notification in Admin Portal. In case we want to show a toast notification in Admin Portal, here is how:
showToastNotification
: general function to show a toast notification.showToastNotificationSuccess
: show success notification, basically runshowToastNotification
with success params.showToastNotificationError
: show error notification. When error given isAxiosError
, this function will try to extracterrorCode
and get translation for the error code. You can pass a default error message when no error code found.
import { showToastNotification, showToastNotificationSuccess, showToastNotificationError, ToastType } from "src/utils";
showToastNotification({ appearance: ToastType.info, message: "Info message here" });
showToastNotificationSuccess("Success message here");
showToastNotificationError(errorObject, "Default error message here");
In case you run this extension without Admin Portal, this notification will only do console.log
We use i18next for localization, for now we have en-US and zh-CN. the default locale is en-US.
import { t } from "src/utils/i18n/i18n";
<Component>{t("translationIdentifiere")}</Component>;
{
"translationIdentifiere": "Translation Here"
}
You don't need to do anything, it will follow Admin Portal's locale.
i18next use localStorage to identify active locale, so you can just add localStorage item then refresh the page.
localStorage.setItem("i18nextLng");
Please check here