Skip to content

Report pages

A report page is a dashboard authored as a single *.report.xml document: filters at the top, then a layout of cards — KPI tiles, charts, a small table, or an existing view embedded read-only. It is served at /reports/{slug} and draws entirely from SQL you author.

It is the third of Genie’s three kinds of report, and the only one that is a page:

Kind Authored as Produces
Code-defined report a C# ReportBase class a downloadable file (PDF, CSV)
Report grid a read-only *.view.xml one interactive, filterable table
Report page *.report.xml a dashboard of tiles, charts and tables

Reach for a report page when one number or one grid is not the answer — when the question is “how are we doing?” and needs several figures side by side.

<Report Name="InventoryOverview" Slug="inventory-overview" Label="Inventory Overview">
<Filters> … the SAME grammar as a view's PreFilters … </Filters>
<DataSets> … read-only SELECTs … </DataSets>
<Widgets> … Tile | Chart | DataTable | View … </Widgets>
<Layout> … the 12-column Grid/Row/Column walk … </Layout>
</Report>
  • <Filters> is the identical PreExecuteFilters block a view declares, rendered by the same panel. Every declared filter is bound to every dataset.
  • <DataSets> are named, read-only SELECT statements. Nothing else is accepted — see the SELECT-only guard.
  • <Widgets> are the cards. The element name is the kind.
  • <Layout> is a form layout, except <Item Name="…"> names a widget instead of a field.

Full element and attribute reference: Authoring a report.

A report page follows the Workflow and Wizard model, not the view model. Its definition lives in the database (Genie.ReportStore), and *.report.xml is a transfer document — you import it, rather than dropping it in models/ for the startup sweep to pick up.

So a *.report.xml sitting on disk is inert until it is imported, and adding *.report.xml to Genie:Migration:FilePatterns does nothing.

There are two ways in, and they write the same document:

  • The report designer — build it visually. Add on the definitions grid creates one and opens it in the designer; the row’s Design action edits an existing one.
  • Import — open System → Automation → Reports and use Import, selecting one or more <Report> files. See Creating a report for the whole sequence.

Either way the definition is created or updated by its Name. Export hands the stored text back byte-for-byte, so a definition round-trips unchanged — which is what lets you design one and then commit the file.

Because a report is not a view. It does not live in the ViewStore’s polymorphic column, it does not share a name namespace with tables and forms, and — like a workflow — it is the kind of artefact an administrator edits in a running system rather than redeploys.

Every import that changes the document appends a row to Genie.ReportStoreHistory under a fresh six-character version id, and repoints the definition at it. So:

  • the current version is a history row, not a special case;
  • re-importing an identical file is reported Unchanged and adds nothing;
  • two imports of identical text are still two versions, each with its own note and timestamp — the id is deliberately random, not a content hash, so neither one is collapsed away.

This mirrors workflow and wizard versioning exactly.

A definition has two documents: the published one, which /reports/{slug} serves, and an optional draft, which is what the designer edits. Only a publish appends a version — a draft save is not versioned at all, which is what makes it cheap enough to press constantly while authoring.

So a report is in one of four states, and the definitions grid names them:

Status Meaning
Never published The row exists (created by Add) but nothing has ever gone live. /reports/{slug} shows an empty page, and the grid’s Open action is hidden.
Draft Never published, and there is work in progress.
Published + Draft Live, with newer unpublished work beside it.
Published Live, with nothing pending.

The published marker is the definition’s version, not the presence of XML: Add writes a minimal parseable stub so the designer can always open something, so a row can have a document and still never have been published.

One draft per report. Publishing clears it; Discard draft throws it away. Importing a *.report.xml publishes directly and ignores drafts entirely, so re-importing over an open draft leaves that draft in place, flagged stale.

A report page is gated by an RBAC resource named after the report, in the Report category — whose defaults are exactly List, View, Export:

<Resource Name="InventoryOverview" Category="Report"
Description="Stock value, replenishment pressure and movement flow"/>

Its data endpoint requires the List verb. Two consequences worth knowing:

  • Structure is not gated beyond authentication. The widget and filter layout is fetched from a separate, SQL-free endpoint — the same split the object endpoints use. This is why an unpublished draft has its own endpoints rather than a flag on this one: a flag here would hand every authenticated user an author’s work in progress.
  • An embedded <View> is gated by its own resource. Embedding never launders a permission: a caller who cannot open low-stock-report sees its error inside the card, not its rows.

A widget can also be narrowed by role with RolesAllowed. A widget the caller’s roles exclude is dropped server-side — its card is not rendered and its dataset is never queried.

Authoring is gated on a different resource: ReportDefinitions, the one behind the definitions grid — View to open a report in the designer, Update to draft or publish it. That separation is deliberate: gating a save on the report’s own resource would make “can view this dashboard” mean “can rewrite it”, and would leave a brand-new report, whose resource does not exist yet, ungated entirely.

The draft preview (/report-preview) sits on the authoring side too, and needs Update — running an unpublished document is more than reading one, and it belongs to whoever may author it.

A draft preview applies the same row-level filter the published report applies, resolved against the report’s own resource name — never against the draft document’s, which the author can change at will. Otherwise renaming a draft would be a way out of a row filter.

Four samples ship in sample/Inventory/models/reports. Between them they exercise the whole vocabulary — a build-failing test (SampleReportFilesTests) parses all four and asserts that every widget kind, every chart kind, both dataset modes and all three filter shapes still have an example.

File Demonstrates
StockPulse.report.xml The smallest valid report: one dataset, one tile, no filters. Start here.
InventoryOverview.report.xml A required DateRange (so the page gates until it is filled), a SingleRow KPI strip, a horizontal bar and a stacked area chart, a Source, a DataTable, and an embedded view.
PurchasingPerformance.report.xml No required filter, so it loads on open. Static-select and boolean filters, Tab blocks, vertical bar and line charts, Percent/Decimal formats, Drill on a tile, and the {{permission-filter}} placeholder.
WarehouseActivity.report.xml A dense layout — a six-tile strip at Span="2", three slice charts to a row, and a chart beside each table — plus Pie and Donut, a collapsed Section, a widget withheld by RolesAllowed, a Hidden widget, a sortable table with a hidden and a role-gated column, two embedded views, and a single-Date filter.

They are transfer documents like any other, so import them from the Reports definitions grid.