internal/api/admin/extensions.go, admin_ui/src/extensions.ts, admin_ui/src/views.ts, gligabao.go
A program that embeds gligabao (see Embedding in Go programs) can extend the admin UI with
- new views, each with a button in the nav on the left, and
- buttons, modals and blocks inside the existing views (for example an extra action in a trained model’s menu).
gligabao.Serve, the server serves it, and the admin page loads it when it starts.
- Quick start
- Registering an extension (Go)
- The JavaScript API:
gb - Views
- Slots: adding to existing views
- A backend for the extension
- Looks
- How it works
- Developing an extension
- Limits and caveats
Quick start
Three files. In your Go program, a folderadmin_ext/hello/ with index.js:
admin_ui = true (or --admin-ui) and open /v1/gb/admin/: a hello button is in the nav.
Registering an extension (Go)
Extensions are passed inServeOptions.AdminExtensions (there is no command-line or configuration-file way to register one, since an extension is code, like the hooks):
Nameidentifies the extension and is part of its URLs. It must be unique.Assetsis anyfs.FS: anembed.FSnarrowed withfs.Sub(as above), oros.DirFS("admin_ext/hello")while developing. It must containindex.js; it may containstyle.css(loaded with the extension) and anything elseindex.jsimports.Handleris described in A backend for the extension.
Serve returns an error if a name is malformed or duplicated, Assets is missing or has no index.js. At most 50 extensions can be registered. Extensions are served only when the admin UI is enabled; without it Serve logs a warning for each and nothing is mounted.
The JavaScript API: gb
index.js is an ES module. Its default export is a function that gets the gb object; it may be async. Everything the extension does starts from there:
The extension can also call gligabao’s own API with
fetch, using absolute paths: fetch("/v1/gb/training/models"), fetch("/v1/gb/admin/api/overview"), and so on. The admin UI is served from the same origin, so no setup is needed.
Editor autocomplete is optional: admin_ui/gligabao-admin.d.ts has the types of everything above. In index.js:
.d.ts next to it, or point the import at it).
Views
elis the view’s empty<div>: draw into it. Wrap content in<section class="card">to look like the built-in views (see Looks).renderruns lazily, on the first click on the button, so an extension that is never opened costs nothing but loading its script. UseonShowto reload data each time the user comes back.- The view is addressed by
#<extension>/<id>in the URL, like the built-in ones (#training), so a reload keeps it open. - The buttons are stacked in the nav in the order the extensions finished loading, after the built-in ones. A long
titlewraps inside the button. - An error thrown by
renderoronShow(or a rejected promise fromrender) is logged to the browser console and listed above the footer of the page; it does not break the rest of the admin UI.
Slots: adding to existing views
A slot is a named place in the admin UI that extensions can add to.gb.slot(name, fn) registers fn(container, ctx); the admin UI calls it each time it draws that place, with the element to add to and a context object that says where it is. Register slots inside the default function, before it returns (extensions load before the admin UI draws its views; a slot registered later is ignored by the places that were drawn already).
<view> is one of main, evals, timeseries, inferences, remember_runs, training (the names of the nav buttons’ data-view): main.top, training.bottom, and so on. Slots of the <view>.top / .bottom / topbar kind are drawn once, when the page starts. A slot nothing was added to is empty and invisible.
A slot function that throws is logged to the console and skipped; the other contributors still run.
Slots are the contract between gligabao and extensions: an extension can only add where gligabao marks a place. Missing a place you need (say, a button next to a dataset)? That is a one-line addition in admin_ui/src: call renderSlot("<name>", element, { ...context }) (from extensions.ts) where the thing is drawn, and add a row to the table above.
A backend for the extension
An extension that needs its own data or actions setsHandler, an ordinary http.Handler:
/v1/gb/admin/ext/billing/api/ with that prefix stripped: the handler above sees /usage. Any method is passed through. The extension’s JavaScript calls it with gb.api / gb.json:
Looks
The extension runs in the admin page, so everything the admin UI’s stylesheet defines is available. Use the same classes as the built-in views and the extension looks native:
Inputs, selects and buttons are styled by element. For anything else, put a
style.css in the extension (loaded with it) and use the palette as CSS variables: --gb-bg, --gb-surface, --gb-border, --gb-text, --gb-muted, --gb-accent, --gb-ok, --gb-err. Prefix your own class names (billing-...): all extensions and the admin UI share one page.
How it works
Servevalidates the extensions and, if the admin UI is on, mounts them next to it:GET /v1/gb/admin/api/extensionslists them:{"api_version": 1, "extensions": [{"name": "billing", "script": "ext/billing/index.js", "style": "ext/billing/style.css"}]}(styleonly if the extension has astyle.css).GET /v1/gb/admin/ext/<name>/<file>serves the extension’sAssets(withCache-Control: no-cache, so a changed extension is picked up by a reload; a request that is notGETthere answers 405)./v1/gb/admin/ext/<name>/api/...goes to the extension’sHandler(any method), if it has one; 404 otherwise.
- When the admin page starts it fetches the list, adds each
style,import()s eachscriptand calls its default export withgb. This happens before the admin UI draws anything, so the extensions’ views and slots are there from the first paint. Extensions load in parallel; one that fails (a syntax error, a missing default export, a throw) or takes longer than 10 s is reported and skipped, the others are unaffected. - Whatever failed to load is logged to the browser console and shown in red above the page footer.
/v1/gb/admin/), and gb.api hides the prefix, so an extension never has to know them. An extension’s own files can import each other with relative paths (import { chart } from "./lib/chart.js"); to use a third-party library, put its browser (ES module) build in the extension’s folder and import it the same way. The admin UI already loads D3 as the global d3.
Developing an extension
During development serve the folder from disk instead of embedding it, and a browser reload is enough to see a change inindex.js or style.css, without rebuilding the Go program:
GET /v1/gb/admin/api/extensions shows what the server registered.
Limits and caveats
- An extension is trusted code. It runs in the admin page with the full rights of whoever uses it: it can call every endpoint of the server. Register only code you wrote or reviewed.
- No access control of its own. The extension’s files and
Handlerare served on the same terms as the admin UI. gligabao has no authentication, so protect the admin UI (a reverse proxy, a private network), and with it the extensions. AHandlerthat does something dangerous should not rely on the extension’s JavaScript being the only caller. - Versioning.
gb.apiVersionandapi_versionof the listing are1. The API changes in a way that can break extensions only with a new version number, and the CHANGELOG says so; a new slot or a newgbfunction is not a break. An extension that needs something recent can checkgb.apiVersionand report it withgb.alert. - Slot context is data, not an interface. Treat
ctxas read-only; do not keep elements you were given beyond the next redraw (the box of a slot is rebuilt each time it is drawn). - No hot add. Extensions are registered at startup and loaded when the page loads; to add or remove one, restart the server and reload the page.
- At most 50 extensions; a name is at most 40 characters.