Menus
The menu drives the React sidebar. One folder structure per app, with leaves pointing at the things the app exposes — a connector query (opens a TableView), a dashboard (opens a DashboardView), an API endpoint (opens an HttpRunner), or a static slug. The tree is pruned to what the caller may run: a leaf whose target the caller cannot run is dropped, and a folder left empty collapses away.
Menus live in config/menus.toml. Hot-reloadable with the rest of the config.
At a glance
type = "query"type = "endpoint"roles = [...]App roots
A menu file declares one or more apps. Each app is the top-level folder in the workspace selector.
[apps.myapp]
label = "My App"
description = "Application backed by the `myapp` connector."
connector = "myapp" # default connector for leaves below
[apps.myapp.l]
fr = "Mon application"
The connector on the app is the default — every leaf inherits it unless it sets its own. Right for apps that bind to one connector; explicit connector on the leaf when crossing.
Items
Each app holds an ordered list of items. An item is either a folder or a leaf.
[[apps.myapp.items]]
label = "Master data"
type = "folder"
[[apps.myapp.items.items]] # nested
label = "Users"
type = "query"
query = "users_get"
screen = "users" # opens the Screen `screens.myapp.users` (else just a grid)
[[apps.myapp.items.items]]
label = "Cities"
type = "query"
query = "cities_get"
[[apps.myapp.items]]
label = "Overview"
type = "dashboard"
dashboard = "overview"
[[apps.myapp.items]]
label = "Health check"
type = "endpoint"
endpoint = "ping"
connector = "myservice" # overrides the app default
Folder
| Field | Description |
|---|---|
type | "folder". |
label / l | Folder title; l = { fr = "…" } translates. |
items | Nested items (folders or leaves). |
An empty folder — every leaf inside dropped by the permission gate — collapses away from the tree.
Leaves
type | What it opens | Required fields |
|---|---|---|
"query" | TableView against connector.query. If screen is set, the row click opens its dialog. | query (+ connector if not the app default) |
"dashboard" | DashboardView for connector.dashboard. | dashboard |
"endpoint" | HttpRunner against connector.endpoint. | endpoint (+ connector) |
"page" | Static React route — handy for a custom screen the framework does not host. | slug |
"link" | External URL — opens in a new tab. | href |
Optional on every leaf:
| Field | Effect |
|---|---|
icon | lucide-react icon name (Users, Database, …). |
roles | List of role names; the leaf is dropped when the caller holds none. |
description | Tooltip / secondary line under the label. |
Permission pruning
The tree returned by GET /api/menus is the caller's tree — anything they cannot run is gone.
| Leaf | Permission required |
|---|---|
type = "query" | sql:<connector>:<query> |
type = "endpoint" | api:<connector>:<endpoint> |
type = "dashboard" | the union of every panel's sql:<connector>:<query> (a panel without permission is dropped — see Dashboards) |
Plus the leaf-level roles filter when set. Folders propagate the pruning upward — when every descendant is gone, the folder vanishes too.
REST endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/menus | Every accessible app's tree. |
GET | /api/menus/{app} | One app's tree. |
The tree is resolved in the request's language (X-Liberty-Lang) — labels come back already translated. The Sidebar renders them straight; no client-side i18n lookup needed for the tree.
Tips & best practices
- One app per business domain. Resist the temptation to bundle a whole tenant under a single app — the workspace selector is built to switch between several. Folders inside an app are the right level for grouping.
- Set
connectoronce on the app. Most leaves stay implicit, the cross-connector ones become visible at a glance. - Pick the right leaf type per target.
queryfor data the operator filters / edits,dashboardfor charts,endpointfor an action the operator triggers without a row context,pagefor a custom React route the framework does not host out of the box. rolesfilter is a soft fence. Permissions enforce the gate;rolesmakes the leaf vanish from the menu so the operator does not see what they cannot run. Use both together — never rely onrolesfor security.- Hot-reload picks up rename / reorder cleanly. Edit
menus.toml, callPOST /admin/reload, refresh the tab — the sidebar repaints. Active TableView / Dashboard panels keep their data; only the tree changes.