Skip to Content

Templates

There are six templates. Each one is a React component that already lives in the repo, and you pick it by name in the template JSON. They all read the same thing: the data-processor rows through the $data tokens. What changes per template is how those rows get drawn, a table, a chart, a set of bars, and so on.

KindNameDraws
listingPHPReactionReport/Widget/ListingTableTemplatea data table
htmlPHPReactionReport/Widget/HTMLTemplateraw markup
summaryPHPReactionReport/Widget/SummaryTemplatea list of label / value rows
meterPHPReactionReport/Widget/AuxiliaryMeterTemplatehorizontal progress bars
gaugePHPReactionReport/Widget/GaugeTemplatea single dial
graphPHPReactionReport/Widget/GraphTemplatea line chart

Columns, headers, and column types work the same across all of them. Those are covered once in the JSON schema; this page is just what each template does with the rows.

How they’re wired (tsx side)

Nothing from the JSON gets compiled. A template name in the JSON is looked up in a plain registry and matched to a component that already ships in the repo.

The registry is a map of name to versions, in src/utils/templates/widgetTemplates.tsx:

export const WIDGET_TEMPLATES = { [WIDGET_TEMPLATE_NAMES.LISTING_TABLE_TEMPLATE]: { 1: ReportListing }, [WIDGET_TEMPLATE_NAMES.HTML_TEMPLATE]: { 1: ReportHtml }, [WIDGET_TEMPLATE_NAMES.SUMMARY_TEMPLATE]: { 1: ReportSummary }, [WIDGET_TEMPLATE_NAMES.AUXILIARY_METER_TEMPLATE]: { 1: ReportMeter }, [WIDGET_TEMPLATE_NAMES.GAUGE_TEMPLATE]: { 1: ReportGauge }, [WIDGET_TEMPLATE_NAMES.GRAPH_TEMPLATE]: { 1: ReportGraph }, };

resolveTemplate(name) splits the name into base and version, looks up the base, and returns the component. No version, or :LATEST, resolves to the highest number registered; :2 asks for that exact one. An unknown base or version renders an error box instead of guessing.

Every template gets the same props, WidgetTemplateProps: data (the rows), columns, headers, variant, search, html, htmlTrusted, emptyMessage. A template just reads the props it cares about, the graph reads columns/headers, the HTML template reads html, and so on. That shared shape is why one render path feeds all six.

ReportRenderClient (src/containers/ReportRenderClient/index.tsx) is that path. Per widget it:

  1. reads the template name from the descriptor (or the widget metadata),
  2. resolves the component from the registry,
  3. turns the descriptor’s columns / column types into concrete rows (columnFormat.ts),
  4. renders the component inside a BaseBox, or an error box if the JSON did not validate.

So adding a template is two moves and no render-path change: register the component under a new name (or add a 2: next to the 1: for a new version), and, if it is a new kind, add its data.<kind> block in templateName.ts. The descriptor parser and the render client stay as they are.

Listing

A full data table. This is the one with the most on it: search box, CSV / XLSX / JSON export, saved column preferences, and a compact sidebox variant. Columns come from data.listing, either dynamic ("$data") or an explicit list with per-column map, header, and type.

Two extras live here too and both render: a per-row progression bar colored by ratio, and a nested tree that indents rows from a parent pointer.

Everything about the listing block is written up in the JSON schema.

A rendered listing report

HTML

Renders markup straight into the widget box. sourceMap is the only key, and it is either literal markup you write, or a $data.<field> that pulls markup out of the row.

{ "template": { "name": "PHPReactionReport/Widget/HTMLTemplate", "version": 1 }, "display": { "title": "Notes", "colSpan": 6 }, "data": { "html": { "sourceMap": "<div>Anything here</div>" } }, }

Literal markup is rendered as-is. Markup that comes from $data.<field> is cleaned with DOMPurify first, since it is data and not something you wrote. See data.html.

Summary

A stacked list of label: value rows. Good for a few totals or a small rollup where a table is too much.

It reads your rows two ways:

  • One row of fields: every key in the first row becomes a line (success is skipped).
  • A list of { label, value } rows: each row becomes a line.

Numbers pass through as-is, and any key with percent in its name gets a % on the end.

// one row, each field is a line [{ "orders": 128, "revenue": 45210, "conversion_percent": 3.2 }] // or a list of label/value rows [ { "label": "Orders", "value": 128 }, { "label": "Revenue", "value": 45210 }, ]

Meter

Horizontal bars, one per metric. Each bar fills to value / max.

It builds the metrics from your rows in this order:

  • A list of { label, value } rows: one bar each.
  • Otherwise the first two columns you name are used as label and value.
  • Otherwise every numeric field in the first row becomes a bar.

The ceiling for each bar comes from a target, max, total, or goal field on the row if one is present, otherwise the bar just shows the value.

[ { "label": "Q1", "value": 80, "target": 100 }, { "label": "Q2", "value": 120, "target": 100 }, ]

Gauge

A single dial for one number. It takes the first metric it can find, the same way the meter does (a { label, value } row, or the first two columns, or the first numeric field), and the ceiling comes from a target / max / total / goal field, defaulting to 100.

[{ "label": "Uptime", "value": 99.5, "max": 100 }]

Graph

A line chart. The first column is the x-axis, and every other column that holds numbers becomes a line. Header labels name the lines in the legend.

// month is the x-axis; sales and returns are two lines [ { "month": "Jan", "sales": 400, "returns": 20 }, { "month": "Feb", "sales": 480, "returns": 18 }, ]

Point the columns at the fields you want with data.graph:

{ "template": { "name": "PHPReactionReport/Widget/GraphTemplate", "version": 1 }, "display": { "title": "Sales", "colSpan": 6 }, "data": { "graph": { "columns": ["month", "sales", "returns"], "headers": "$data" } }, }
Last updated on