Creating a new dashboard from scratch
No endpoint, no data shaped for a dashboard, nothing to copy. You're building all four layers.
If the endpoints already exist, you want Migrating an existing dashboard instead — that path is usually much shorter.
Plan first
Answer three questions before touching anything.
1. Who sees this?
One audience or several? That decides your tab structure, because tabs are how you split audiences — one config with permission-gated tabs, never two configs.
A user holding only code A never sees Tab B — and can't call its endpoint either.
2. What question does each widget answer?
If you can't say it in one sentence, cut the widget.
3. What does each tab's payload look like?
Sketch it before you write it. The payload is a contract between three layers, and getting it right on paper is much cheaper than getting it right three times in code.
Widgets bound to a key that no longer exists render empty. Nothing raises, nothing appears in Grafana. That's exactly why the contract is worth writing down.
Part A — Database
1. Write one stored procedure per tab. The convention is one sp per tab.
Three rules keep these consistent:
| Rule | Why |
|---|---|
| Scope everything to the company | Chase the relationship down to the partners table and filter on the company id. |
| Date params are optional, end date inclusive | An unset filter means unbounded, not empty. |
| Return one row per thing you'll chart | Do the grouping in SQL. Neither the endpoint nor the config should be reshaping arrays. |
2. Smoke-test every result type against a real company id, once with no date range, once with one, before anything downstream depends on the shape.
3. Create the permission codes if not already created.
4. Attach the codes to permission groups.
A permission code that belongs to no group grants nobody anything. It is the single most common reason a finished dashboard shows up empty.
Do it through our Portal Settings → Permissions as an Admin.
5. Run it all on dev. Staging and production come later, at elevation.
Part B — Backend
6. Add one route and one action per tab. The config model is one API call per tab, so each action calls the stored procedure once per result type and assembles a single response.
Four things to get right:
| Do this | Why |
|---|---|
| Bundle the whole tab into one response | KPIs, chart series, filter options, drill-through ids — one round trip per tab. |
| Gate with the any-of permission check | The all-of helper would demand the tab code and the admin code. The any-of check passes on either, and short-circuits for system records. |
| Localize labels server-side | Return bucket names and status descriptions already translated, so the config binds to a name field and never hard-codes UI text. |
| Accept the filter params | Whatever the dashboard will send |
7. Call each endpoint and keep the response open. Every $ref path you write
in the next part is a path into that document.
Part C — Studio Builder
Sign in as a system administrator: Settings → Dashboard Builder → Create dashboard.
8. Guided setup asks four things:
| Field | What to pick |
|---|---|
| Name | Internal only — never shown to end users. Make it descriptive. |
| App mode | payroll, edi, or ai_platform. |
| Scope | Specific company, or Global default to replace the dashboard everyone sees. |
| Inheritance | Start fresh when this replaces the default entirely. |
9. Build the first tab. In Tab settings, give it a key (stable, lowercase — it goes in the URL), a label translation key, an icon, and the permission codes that gate it. Then, still in Tab settings:
- Data sources — add
main, point it at your endpoint, and bind each param to a filter. AdateRangefilter splits into two halves addressed with dotted keys,period.startandperiod.end. - Filters — in the order you want them in the toolbar.
- Actions — toolbar buttons. Exports are usually
exportCsv.
10. Add the widgets in display order. Canvas order is render order.
PathField suggestions come from the sample fixture, so a widget renders on the canvas the moment you fill it in. Building several similar charts? Build the first one fully, then duplicate with Ctrl+D — usually only the label, paths and color differ.
Shapes and layout are real; the numbers are fixtures. You verify against real data in Part D.
11. Add the remaining tabs. Click + and repeat. Each tab is fully
independent — its own data sources, filters, actions and widgets.
12. Layout pass. Cycle the viewport toggle. Resizing edits that one breakpoint:
| Viewport | Breakpoint edited |
|---|---|
| Desktop | lg |
| Laptop | md |
| Tablet | sm |
| Phone | xs |
13. Validate, then Save & activate. Target: Valid, 0 errors, 0 warnings — click the chip to jump to any offending widget. Activating deactivates the prior config for that scope and inserts this one as a new revision; the old one stays in the history drawer.
Part D — Verify
14. Check every audience. Sign in without each gating code, then as an admin.
The tab being absent rather than hidden is the point — the Resolver strips it server-side, so there is no element in the DOM to find. A user without a tab's code should also get a 403 from that tab's endpoint.
15. Check the data and the interactions.
- Every card and chart shows real numbers. A blank one is a wrong
$refpath, not a bug, missing nodes resolve tonullsilently. - Filters change the data and update the URL.
- Click-throughs land on real pages, with no
{placeholder}left in the URL. - The Network panel shows one
/dashboards/…call per tab. - Both languages render with no console errors.
16. Promote to stage and prod, in this order. The config is useless without the layers under it.
Step 4 is the only one that isn't a normal deploy: configs live in the database, so you copy the JSON out of the dev Builder and paste it into the target environment's Builder through JSON tools. Validation runs there too.
Then re-check permissions in the target environment. A pasted config can name a code that doesn't exist there yet, and the Validator won't catch it — the tab just silently disappears for everyone.
17. 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.
Checklist
Database
- One stored procedure per tab, sliced by result type
- Company-scoped, with an optional inclusive date range
- Smoke-tested per result type against a real company id
- Permission codes inserted idempotently
- Codes attached to permission groups ← the step people miss
Backend
- One route and one action per tab
- One bundled response per tab
- Gated with the any-of permission check (tab code or admin code)
- Labels localized server-side
- Response captured and kept open while building
Studio Builder
- Guided setup: name, app mode, scope, inheritance
- Tab key, label, icon, permissions
- Data source
main, params bound to filters - Filters and actions recreated
- Widgets in display order, with source, grid and paths set
- Layout pass across all four viewports
- Validation chip reads Valid
- Save & activate
Verify
- Every audience sees exactly its own tabs
- Numbers correct, filters work, click-throughs resolve
- One request per tab
- Both languages, no console errors
- Promoted in order: database → backend → permission groups → config