Build an MDK UI page for worker/plugin data
MDK UI shells (apps/dashboard/, scaffolded by mdk create dashboard — or
apps/<name>/ if named explicitly) are composition only. Data comes from
a Gateway route; shaping lives in a hook; visuals come from
@tetherto/mdk-react-devkit. Never invent component prop names — look them
up in references/ui-registry.json.
Real reference implementation, shipped in every scaffolded dashboard — copy
its shape for a new page, then delete it once you don't need the example
(see the shell's own USAGE.md, "Worked example: System Info"):
| Layer | File |
|---|---|
| Foundation query | @tetherto/mdk-ui-foundation src/query/factories.ts (siteQuery/userInfoQuery) |
| Hook | @tetherto/mdk-react-adapter src/hooks/use-system-info.ts |
| Page | apps/dashboard/src/pages/SystemInfo.tsx |
| Panel | apps/dashboard/src/components/SystemInfoPanel.tsx |
| Route | apps/dashboard/src/routes.ts |
Prerequisites (do these first)
- Data source exists. If no
/api/...route returns the metric, stop and usemdk-gateway-pluginto create one. Confirm withcurlthat the JSON shape is stable. - Contract units known. Read the worker
mdk-contract.jsonso labels/units match (power→W, etc.). Resolve it frommdk.yaml→spec.workers: localpackage→<package>/mdk-contract.json(or legacy<package>/plugin/mdk-contract.json); npm package → the same undernode_modules/<package>/. - Dashboard app exists. If missing:
mdk create dashboard(seemdk-deployment).
Workflow
flowchart TD
intent["Plain-language intent"]
route{"/api route returns the metric?"}
gateway["mdk-gateway-plugin: create the route"]
registry["Pick components from ui-registry.json"]
contract["Match labels/units to the worker mdk-contract.json"]
build["Build the hook, panel, and page"]
wire["Wire the route in routes.ts"]
intent --> route
route -->|no| gateway --> registry
route -->|yes| registry
registry --> contract
contract --> build --> wire
1. Pick the visual from the registry
Open references/ui-registry.json (this skill). Use
indexes.componentsByName / indexes.componentsByCategory.
| Need | Typical component |
|---|---|
| Single numeric metric / total | SingleStatCard |
| Time-series | LineChartCard |
| Fallback text when null | FALLBACK constant |
Copy exact prop names and types from the registry entry. Example
SingleStatCard: name, subtitle, value, unit, variant, flash.
2. Add the page scaffold
Prefer the CLI so routes.ts stays tool-compatible:
cd <dashboard-app>
npx mdk-ui add page <PageName>
That appends a one-line entry to src/routes.ts above // mdk:routes-end.
If you edit routes.ts by hand, keep each route on a single line and
preserve the end marker.
3. Create the data hook (shape the payload)
For a project-local plugin (one scaffolded under plugins/, referenced by
its package name in mdk.yaml → spec.gateway.plugins), put the hook in the
app:
src/hooks/use-<thing>.ts
import { useQuery } from '@tetherto/mdk-react-adapter'
// types matching the plugin JSON schema
export const useThing = () => {
const result = useQuery({
queryKey: ['thing'],
queryFn: async () => {
const res = await fetch('/api/...')
if (!res.ok) throw new Error(`Failed (HTTP ${res.status})`)
const raw = await res.json()
// shape → render-ready; never leave shaping to the panel
return { /* ... */ }
},
refetchInterval: 5_000, // live tiles
})
return {
info: result.data ?? EMPTY,
isLoading: result.isLoading,
error: result.error,
refetch: () => { void result.refetch() },
}
}
Rules:
- Import
useQueryfrom@tetherto/mdk-react-adapter, not from@tanstack/react-querydirectly. - Hook owns fetch + shaping; panel stays presentational.
- For reusable MDK-wide endpoints, prefer adding the hook to
@tetherto/mdk-react-adapter(see dashboardUSAGE.mdSystem Info flow). App-local hooks are correct for project-local plugins.
4. Create the presentational panel
src/components/<Name>Panel.tsx
- Props: already-shaped
info+isLoading+error - No
fetch, no stores, no inline transforms beyond trivial display (Math.round) - Handle three states:
error, empty list, data - Render registry components with verified props
5. Wire a thin page
src/pages/<Name>.tsx — mirror SystemInfo.tsx (the shipped reference page):
- Call the hook
- Wrap in
PageLayoutwith title + optional Refresh action - Pass hook outputs to the panel
- Page holds no fetch / shaping
6. Styles
Add a BEM block under the shell namespace in src/App.scss, e.g.
.mdk-ui-shell-<name>, matching the className on PageLayout. Reuse CSS
variables (--mdk-color-*). Mirror the .mdk-ui-shell-system-info block
already in the file.
Composition rules (do not break)
From the dashboard USAGE.md:
Gateway plugin / foundation query
→ adapter or app hook (fetch + shape)
→ thin page (hook → panel)
→ presentational panel + @tetherto/mdk-react-devkit
Forbidden:
| Don't | Do |
|---|---|
fetch inside a panel/component |
Hook |
Guess SingleStatCard props |
references/ui-registry.json |
| Import Ant Design / MUI | @tetherto/mdk-react-devkit |
Edit router.tsx for feature pages |
routes.ts via mdk-ui add page |
| Hard-code units ignoring the contract | Use unit from the API payload |
Worked recipe: "UI to show <metric> for <device family>"
End-to-end chain this kind of prompt implies (router skill expands this):
mdk-gateway-plugin— ensureGET /api/<domain>/<resource>exists and returns a shaped payload (e.g.{ unit, total, devices[] }) grounded in the worker contract's telemetry channel.- This skill — hook +
SingleStatCardgrid (total + per-device) + page + route. mdk-deployment— plugin listed inmdk.yaml, gateway running, worker online with a device.
Detailed file checklist: references/page-recipe.md.
Hand-off
| Problem | Skill |
|---|---|
| No API route / wrong shape | mdk-gateway-plugin |
| Stack not running / plugin not loaded | mdk-deployment |
| Device / telemetry channel missing | mdk-worker-plugin |