Skip to main content

Migrating an existing dashboard to a config

This is the path for a dashboard that already exists as a React route. The goal is to re-express it as a config and delete the legacy route.

Migration is usually the easy direction: the endpoints already exist, so most of the work is inventory and layout, not backend. Some dashboards can be migrated with no database and no backend work at all.

Before you start: you need a system-record account, and you should work on dev. Keep the legacy page open in one tab and the Builder in another.


Part A — Inventory

Twenty minutes here saves hours later.

1. List every visual on the legacy page, top to bottom. You're making a shopping list of widgets.

2. Map each one to a widget type.

Legacy thingConfig widget
Row of small metric cardsstatCards
One big number, maybe a sparklinekpiCard
Bar / column chartbarChart
Line or area chartlineChart
Pie or donutpieChart
Bars plus a pie breakdowncombinedBarPie
Year-of-days calendar gridcalendarHeatmap
X×Y matrix heatmapheatmap
Field list / detail panelkeyValueList
Labeled progress barsprogressList
Data grid tabledataTable
Map with routes or dotsmap
Two views behind a toggletabbedGroup of two widgets
Widgets stacked in one columngroup
None of the abovechart (raw ECharts)

Two patterns to know up front:

  • A view toggle is a tabbedGroup, not a bespoke control — two full widgets, one per tab.
  • Per-bar colors need the raw chart type. The sugar barChart colors by series, not by bar. That's the usual reason to drop to a raw ECharts option.

3. Note the filters, buttons and click-throughs. Everything on the legacy page has a home in the config:

Write down which filter feeds which request param — you'll need that exact mapping.

4. Open the Network tab and count the requests. This is the step people skip, and it decides how much backend work you have.


Part B — Database

5. In most migrations, do nothing here. The stored procedures already exist. Skip to Part C.

6. Write SQL only if the inventory found a gap. Two cases:

GapWhat to do
A value lives only in the frontend — coordinates, thresholds, anything the React code hardcodes or computesIt has to move server-side. A config holds no logic, by design.
You're adding something genuinely newFollow Creating a dashboard from scratch, Part A.

If you do write SQL, follow the convention: one procedure per tab, sliced by result type, one result set per call.


Part C — Backend

7. Decide whether the current payload works as-is. The model wants one endpoint per tab, returning everything that tab needs.

Extra data sources for dependent dropdowns are the sanctioned exception, not a violation. A tab with a main source plus three small option lists is fine.

8. If you're bundling, add a new action rather than changing the old one. The legacy route is still live during migration; don't break it.

9. Localize labels server-side. Bucket names, status descriptions, purpose codes — return them already translated. The config then binds to a name field instead of hard-coding UI text, and the second language works for free.

10. Confirm the endpoint enforces its own permissions. Config-level requiredPermissions is defense in depth, not the gate. Use the any-of permission check so the tab code or the admin code passes; the all-of helper alone would demand both.

11. Call the endpoint and keep the response open. Every $ref path you type in the Builder is a path into that document.


Part D — Studio Builder

12. Create the config. Settings → Dashboard Builder → Create dashboard:

FieldWhat to pick
NameInternal only — a descriptive name beats "Dashboard 2".
App modepayroll, edi, or ai_platform.
ScopeGlobal default if you're replacing the dashboard everyone sees; Specific company for one customer's page.
InheritanceStart fresh if this replaces the default entirely, otherwise Default tabs first / Default tabs after.

13. Configure the tab. In Tab settings, set a stable lowercase key (it goes in the URL), reuse the legacy page's translation key for the label — don't invent a new one — then pick the icon and the permission codes that gated the legacy route. Still in Tab settings:

  • Data sources — add main with your endpoint, binding each param to its filter. A dateRange splits into two params with dotted keys, period.start and period.end.
  • Filters — same order as the legacy toolbar. For a dropdown fed by another endpoint, point its options at that data source, and use dependsOn for cascades.
  • Actions — recreate the buttons. Exports are usually exportCsv.

14. Add the widgets top to bottom — canvas order is render order.

Hex works anywhere a color is accepted, so legacy colors carry over exactly. Click-throughs become On clicknavigate with a template, or openModal with the existing modalId. Building six similar charts? Build the first fully, then Ctrl+D; usually only the label, paths and color differ.

The canvas shows sample data, not your endpoint

The Builder never calls live endpoints. Shapes look right, numbers won't match production. You verify real data after activating, in Part E.

15. Layout pass. Cycle the viewport toggle. Resizing edits that one breakpoint:

ViewportBreakpoint edited
Desktoplg
Laptopmd
Tabletsm
Phonexs

Everything should be full-width on Phone.

16. Get to Valid. Click the chip to jump to any offending widget. The two warnings you'll actually see:

  • "declares N DataSources" — fine if it's deliberate (step 7).
  • "source is missing" — you forgot to set Source on a widget. Fix it.

17. Save. Save as draft while you're still comparing; Save & activate when ready. Activating deactivates the previous config for that scope and inserts this one as a new revision. The old one stays in the history drawer.


Part E — Verify, then sunset

18. Compare side by side with the legacy page, signed in as a user of the target company:

  • Every number matches. A blank card means a wrong $ref path — missing nodes resolve to null rather than throwing, so a typo shows as empty, never an error.
  • Filters change the data and update the URL.
  • Click-throughs land on real pages, with no {placeholder} left in the URL.
  • Exports download.
  • The Network tab shows one /dashboards/… request per tab.
  • Both languages render with no console errors.

19. Test permissions properly. Sign in without each gating code and confirm the tab or widget is absent — not hidden, absent. Then confirm an admin sees everything.

20. Promote to stage and prod. If Part B added SQL or Part C added an endpoint, ship those first. Then move the config itself with JSON tools → Copy on dev, and paste → Apply to canvas → Save & activate in the target environment's Builder. Validation runs there too.

Spot-check permissions afterwards: a pasted config can name a permission code that doesn't exist in the target environment, and the Validator won't catch it — the element just silently disappears for everyone.

You don't need to file the JSON anywhere. Every save is a new row, so the revision-history drawer is the backup and Restore is the rollback. Use JSON tools → Download only if you want an off-box copy for a review or a ticket.

21. Redirect the legacy route, then delete it. Redirect first — the route keeps working for anyone with a bookmark while you watch for regressions.

For dashboards shared across several companies, wait until every company using one has a config live before deleting anything.


Gotchas

Symptom or trapWhat's actually going on
A blank widgetA bad path, not a bug. Missing nodes resolve to null or [] silently — check against your saved response.
$ref pathsThey start with the data source name: main.kpis.total, not kpis.total. The Validator catches a wrong source name, not a wrong path inside it.
The data envelopeAlready unwrapped for you. Write main.kpis, not main.data.kpis.
Duplicate widget keysKeys must be unique within a tab. Ctrl+D handles this; hand-pasted JSON doesn't.
Frontend arithmeticAnything the React code computes has to move server-side first. There's no config-level arithmetic, by design.
Hand-writing JSON to save timeDon't. The Builder catches mistakes as you make them. Pasting through JSON tools is for restoring a backup, not authoring.