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_PATH | No | The base path of extension |
JUSTICE_ADMINPORTAL_URL | No | The url of the admin portal website. Required for cross-domain communications. |
JUSTICE_ADMINPORTAL_ROUTE_PREFIX | No | The prefix path that will be used inside the admin portal website. |
JUSTICE_BASE_URL | Yes | The base url of the API which the Admin Portal will call. Required if the extension want to be run without admin portal |
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 |
EXTENSION_CLIENT_NAME | Yes | Client name that will showed in admin portal side menu |
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-extension-accelbyte-marketplace-moderation" // icon that will be shown in AP sidebar, refer to icons in AP or public/extension-icons if enabled
}
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>
);
WARNING:
io-ts
andguardNetworkCall
are still available only for old response models that are already exist before vite update. They are deprecated and will soon be removed completely. For future work, please usezod
andValidate.responseType
instead as described below.
We do network call with axios and runtime checking with zod. 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 zod
to make sure we get expected response data. Please check zod for complete guidelines.
export const Item = z.intersection(
z.object({
itemId: z.string(),
}),
z
.object({
description: z.string(),
})
.partial()
);
export type Item = z.TypeOf<typeof Item>;
export class ItemDecodeError extends DecodeError {}
We mainly use responseType
in Validate
from accelbyte-web-sdk 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
guardNetworkCall
will always has this return object
const { response, error } = await Validate.responseType(...params);
response
will be null when an error thrown inside responseType
and vice versa. Any error thrown will be in error
(network call error like 400, runtime check error).
import { z } from "zod";
import { Validate } from "@accelbyte/sdk";
import { AxiosInstance } from "axios";
export function fetchSomething(network: AxiosInstance, namespace: string) {
return Validate.responseType(() => network.get(`/endpoint`), z.array(Item));
}
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>;
{
"translationIdentifier": "Translation Here"
}
The communication between Admin Portal and Admin Portal Extension is done using postMessage
in order to successfully communicate between each other when each has a different domain than one another. If you want to send a MessageEvent
to Admin Portal and expect a return message from it, you can use guardSendAndReceiveMessage
. Here's how you do it:
- Add the message type here (if it's a new message type)
- Add the response's data Codec here (if it's a new response Codec)
- Create the message event data using SendMessageEvent interface
- Call
guardSendAndReceiveMessage
and use the message event data, Codec, and timeout, as the parameters. Timeout, here, is used to set the duration (in milliseconds) of the message listener before it is closed. - The method will return the data sent by Admin Portal.
Note:
- If you only want to send a message to Admin Portal without expecting a response, you can do step 1-3 and call
sendMessageToParentWindow
instead ofguardSendAndReceiveMessage
- Make sure the handler for a certain message type is already exist in Admin Portal, especially for a new message type. Otherwise, you have to create the handler in Admin Portal beforehand
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
- Uncomment
icon_path
inscripts/generateExtensionModule.js
- Follow the step above
Generating New Icon
, the difference is that, you will usepublic/extension-icons/extension_icons.svg
as the source - copy the generated fonts to
public/extension-icons/
- Modify
extension_icons.css
with the desired class name and apply it to the respectivemodules.json
The css naming convention format for extension icons are:
.icon-extension-<client's name>-<icon name in kebab case>
Please check here