Module Development
Though we do not have specific restrictions on languages you can use (since we're a microservice architecture), we
have samples and tooling for Java (Spring Boot) as well as Golang for the backend services, and NodeJS (SvelteKit)
for the web.
Repository Structure
To keep things manageable, we aim to have every module contained within one repository - a monorepo. We separate
services or applications within a module by folder. For example, with the Dashboard Module, it has a ./web folder,
containing a Web Base-based project that deals with the modlet development, and an ./app folder written in Golang
that handles the backend API.
We also have additional tools installed in each repository to handle:
- Code Cleanliness
- Code Coverage
- Conventional Commits
- Change Management
- CI/CD Configuration
These are (or will be) elaborated in the Source Control section, but their place in the monorepository is described here:
.
├─.changeset
├─.github
├─.husky
├─.commitlintrc.cts
├─.gitignore
├─package.json
├─README.md
├─<project-folder-a>
...
The above shows the typical minimal structure for a monorepo. Even if your service is not a NodeJS module, you would
have a package.json which has tools to mainly handle 2 things:
- Triggering automations on git hooks via Husky
- Handling change management via Changesets
Each of the above tools are npm packages.
The following list explains what each folder is for. Their specific individual configurations are currently beyond the scope of this document as they can be large topics on their own.
-
.changesetWe can track the "entire" version of the module with
package.json. We usechangesets(configuration within the.changesetfolder) and the npm workspace concept to handle multiple projects. -
.huskyThe folder holds the configuration for Husky - we can run custom scripts here before any code is checked in, such as whether your commit messages follow our conventions, whether your code is free of TypeScript or linting errors, and whether your code has a certain code coverage percentage, or all unit tests pass.
-
.githubThis folder holds GitHub-specific files, such as GitHub Actions workflows (for our CI/CD), the
CODEOWNERSfiles, etc. -
.commitlintrc.ctsThis file contains the commit linting conventions - used to ensure commits follow the format we want.
-
<project-folder-a>Create folders, give them an appropriate short name, and place your individual projects in there. One folder per project.
Containers
Local Development Environment (Docker Compose)
Each module should have it's respective docker-compose.yml file, maintained inside our
dev-containers repository. To add your module in, you should create
a new folder with your module's short name, and place your docker-compose.yml files in there. We shorten the name of
the file to compose.yml for brevity, but keeping the "compose" name to keep VSCode's Intellisense with the Docker
plugin working.
Each compose.yml file uses the include
feature to deal with dependencies on other modules/containers.
The complication comes from having traefik as the ingress controller in our compose files. In the infra "module"
within the dev-containers, we have traefik running to help route all trafic based on the hostname. We also use
traefik to handle CORS via middlewares. Your compose.yml must take this into account. traefik allows you to use
labels in Docker to provide this functionality, you can learn more about using traefik in their
official documentation.
Modlets (Frontend / Web)
Modlets are what we call the web portions of each module. They comprise of all the web (Svelte) components, web
libraries, routes and API endpoints that make up the web portions of a module. These components are packaged into
npm node modules in a format that allows them to be easily installed using our CLI.
When developing your custom modlet on top of the Web Base, it's essential to follow a few standards to for it to be
easily packaged for distribution.
CLI Tool Packaging Requirements
Our Web Base features a built-in packaging feature that streamlines the process of extracting and publishing your
modlets to our registry.
Required Folders
To ensure seamless integration with the Web Base, you must compartmentalize the files that need to be exported,
making it easy for others to use your modlet.
In the following sections, the <project-name> is currently hard-coded to "aoh" in the Web Base CLI, we'll have to
add proper configuration for this on the next release.
-
Components / modlet folder:
Store all your reusable components and modlets, such as Svelte components, in a single folder.
src/lib/<project-name>/<module-name>- we refer to this as the modlet's lib folder
-
Routes (public and private) folders:
SvelteKit does not support package routes, to circumvent this we also use a standardized structure to allow us to have the CLI tool copy routes to their appropriate locations. Organize your routes into two separate folders - one set under
(public)for routes that do not require access control, and the other set under(private)for routes that do require access control.src/routes/(public)/<project-name>/<module-name>src/routes/(private)/<project-name>/<module-name>
By adhering to our recommended folder structure, you'll enable the packaging manager to easily identify and extract the necessary files for packaging as well as publish your modlets to our registry without errors or complications.
Example Folder Structure
The following example is for a project named foo and a modlet/module named bar.
...
src
├─lib
│ └─foo
│ └─bar
│ └─ <whatever files you put under here is up to you>
├─routes
│ ├─(private)
│ │ └─foo
│ │ └─bar
│ │ └── <whatever files you put under here is up to you>
│ └─(public)
│ └─foo
│ └─bar
│ └── <whatever files you put under here is up to you>
package.json
...
The structure of the contents within these folders are entirely up to you. You are free to organize your files and
create more folders as you see fit as long as they are contained within these 3. They are namespaced by project and
module names (foo and bar respectively) to avoid name clashes.
We recommend keeping Svelte components to a folder called "components".
Required files
modlet.config.ts
src/routes/<project-name>/<module-name>/modlet.config.ts
This is the runtime configuration file for the modulet, at present, each modlet supplies our headerbar and sidebar
with configuration data to determine the names, icons, and menu items to show in when the modlet is in use.
The file must export a const config object, containing the nav field. This is used by the headerbar and sidebar.
Any other fields can be defined by you (the modlet developer) for use in your modlet if you need to a way to pass
configuration data around.
The nav field is an object that contains:
{
code: string,
header: { name: string, url: string },
sidebar: Array<{ name: string, icon: string, url: string}>
}
Description
-
codeThe
codeis a string used to identify the modlet - it is the modlet's short name. For example, for theDashboard Module, it is "DASH". -
headerThe
headeris an object containing:name: string
The of the module to display in the module selector (in the headerbar).
url: string
The path to the page to display when selecting the module from the headerbar.
-
sidebarThe
sidebaris an object containing an array of menu items to appear in the sidebar for that module. Each menu item consists of:name: string
The name of the menu item to display
icon: ComponentIcon
A lucide-svelte icon to render
url: string
The path to the page to display when selecting the menu item.
import type { ComponentIcon } from "lucide-svelte";
import ChartLine from "lucide-svelte/icons/chart-line";
import Star from "lucide-svelte/icons/star";
//This is a placeholder type, will update with real type
export type DashboardConfiguration = unknown;
export interface DashboardModletConfiguration extends ModletConfiguration {
dash: DashboardConfiguration;
}
export const config: DashboardConfiguration = {
nav: {
code: "DASH",
header: {
name: "Dashboard",
url: "/aoh/dash",
},
sidebar: [
{
name: "Favorite Dashboards",
icon: Star as unknown as ComponentIcon,
url: "/aoh/dash/favourite",
},
{
name: "Dashboard Management",
icon: ChartLine as unknown as ComponentIcon,
url: "/aoh/dash",
},
],
},
};
modlet.setup.ts
src/routes/<project-name>/<module-name>/modlet.setup.ts
This file is to supply installation-time configuration for the modlet. It must export a const setup, that has 2
optional fields:
envA map of objects - each containing the "value" and "description" of the environment variable. The key of the map is the key of the environment variable. This will be used to populate the.env.templatewhen the modlet is installed in theWeb Base.postInstallationAn asynchronous function that returns aPromise<boolean>- true or false for success or failure respectively. You can do anything in this function, and the CLI tool will run it. For example, in the case of theGIS, it modifies thevite.config.tsto add extra configuration values.
export const setup: ModletSetupConfig = {
env: {
GIS_URL: { value: "http://gis.127.0.0.1.nip.io", description: "The URL to the GIS backend service" },
PUBLIC_RTUS_SEH_URL: {
value: "http://rtus-seh.127.0.0.1.nip.io",
description: "The URL to the RTUS Server Sent Events service",
},
PUBLIC_GIS_RTUS_MAP_NAME: {
value: "gis",
description: "The name of the distributed map to connect to in RTUS",
},
},
postInstallation: async () => {
try {
// This function is too long to list here, but you get that point
await modifyViteConfigInternal();
return true;
} catch (err) {
console.error("\nAn unexpected error occurred attempting to modify `vite.config.ts`:", err);
return false;
}
},
};
Modlet Registration in CLI
For the CLI tool to recoginize your modlet, it needs to be exported in the
config.ts in the CLI.
Below is a sample of the config at the time of writing:
export const config: WebBaseCliConfig = {
modlet: {
dash: { name: "Dashboard", package: "@mssfoobar/dash" },
gis: { name: "Geospatial Information System", package: "@mssfoobar/gis-web" },
iams: { name: "Identity & Access Management Service", package: "@mssfoobar/iams-web" },
ian: { name: "In-App Notifications", package: "@mssfoobar/ian" },
ims: { name: "Incident Management Service", package: "@mssfoobar/ims-web" },
unh: { name: "Unified Notifications Hub", package: "@mssfoobar/unh-web" },
wfe: { name: "Workflow Engine", package: "@mssfoobar/wfe-wfd" },
headerbar: { name: "Headerbar", package: "@mssfoobar/webbase-headerbar" },
sidebar: { name: "Sidebar", package: "@mssfoobar/webbase-sidebar" },
},
};
For example, if your module short name is foo, with the full name Foobar Service, and the package
@mssfoobar/foo-web, you would be adding the following to the config export:
{
"modlet": {
"foo": { "name": "Foobar Service", "package": "@mssfoobar/foo-web" }
}
}
Permissions
When creating modules or modlets, each one should come with its own set of permissions the protect endpoints, control how pages looks etc. Our guidelines are to define who (what role) is intended to access these features, and split them up accordingly. However, you will not be protecting resources with "roles", instead, you will use resources and scopes (referred to here as a permission) to protect them. This is because projects will define their own roles, which has its own fixed names - these should roles should then be given permissions accordingly.
Scopes
By convention, we recommend sticking to just two scopes - view and edit. The view scope essentially referrs to
read-only access over a feature, or module, whereas the edit scope will allow making mutations to resources such as
writing, updating, and deleting on top of reading.
Use the lower case view and edit scopes for your resources. It is unlikely that more will be needed as you should
be able to create different resources and resource types to meet your needs, however, if it makes sense you may create
more scopes.
Resources
Our modules will tend to be split into an "operator" feature, that is, some feature that will be used by people conducting actual operations in a command centre, and an "adminstrative" feature, that is, a feature that is used by administrators or managers during the setup of the system. It is expected that the administrative features might further be split between system-related configuration, and business-related configuration. For example, system configuration would be things like configuration database retention policies or even adding users to the system. Business-related configuration would be to configure pre-defined dashboards for operators to focus on.
Recommendations
To keep things simple, if you cannot envisage how the system be used, it must all check for just one permission. However, we believe most of the time, they will be segregatable into "administrative" vs "operating" resource types.
Summary (Naming Conventions)
For both:
- Use
snake_case
For scopes, stick the following 2, add more only if absolutely necessary:
viewedit
For resources - each resource has a "resource name" and "resource type", both of these should be prefixed by the module's shortname, followed by a period ("."), and then the actual name of the resource in snake_case.
<module_short_name>.<resource_name>
For example:
rnr.admin