Skip to main content

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

📄 menus.toml[apps.myapp]label = "My App"connector = "myapp"[[apps.myapp.items]]label = "Master data"type = "folder"items = [...]leaf · type = "query"label = "Users"query = "users_get"screen = "users"leaf · type = "dashboard"label = "Overview"dashboard = "overview"⚙ permission gatesql:{c}:{q}required for type = "query"leaf dropped otherwiseapi:{c}:{e}required for type = "endpoint"leaf dropped otherwiseroles filterleaf with roles = [...]caller must hold onefolder collapsean empty folderis dropped from the treeGET /api/menusresolved in the request's language⚛️ SIDEBAR📊 Overview📁 Master data📋 Users📋 Cities📋 Roles📁 Operations📊 Daily KPIs🌐 Health check📁 Settings⚙ Users⚙ Roles⚙ Connectorsi18nlabels via l = …

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

FieldDescription
type"folder".
label / lFolder title; l = { fr = "…" } translates.
itemsNested items (folders or leaves).

An empty folder — every leaf inside dropped by the permission gate — collapses away from the tree.

Leaves

typeWhat it opensRequired 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:

FieldEffect
iconlucide-react icon name (Users, Database, …).
rolesList of role names; the leaf is dropped when the caller holds none.
descriptionTooltip / 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.

LeafPermission 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

MethodPathPurpose
GET/api/menusEvery 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 connector once on the app. Most leaves stay implicit, the cross-connector ones become visible at a glance.
  • Pick the right leaf type per target. query for data the operator filters / edits, dashboard for charts, endpoint for an action the operator triggers without a row context, page for a custom React route the framework does not host out of the box.
  • roles filter is a soft fence. Permissions enforce the gate; roles makes the leaf vanish from the menu so the operator does not see what they cannot run. Use both together — never rely on roles for security.
  • Hot-reload picks up rename / reorder cleanly. Edit menus.toml, call POST /admin/reload, refresh the tab — the sidebar repaints. Active TableView / Dashboard panels keep their data; only the tree changes.