Report Layout Widget
A Report Layout widget appears as the visualization inside a Protrak report. Instead of the standard table that Protrak generates, your widget receives the processed report data and renders it however you like — a custom table, a pivot, a chart.
Reports are not visible in the mobile app.
How reports work in Protrak
When a user opens a report, the platform:
- Runs a custom program (C# server-side code) to process and aggregate the raw data
- Passes the result to your custom widget as
pageContext.data - Your widget renders the data visually
The custom program and the custom widget are configured together in the Report Layout admin.
pageContext shape
Source: CustomReport.jsx
{
data, // Processed/aggregated rows ready for display (from the custom program)
totalData, // Raw array of all records before aggregation
config, // Report layout configuration from Admin (group, fields, filters, etc.)
filters, // { columnName, columnValue, filterValue } — populated when drilldown is active
instanceType, // Type name (mapped from typeName in source)
typePluralName, // Plural display name
// Sorting
sortState, // { sortedBy, isDescending }
onSortApplied, // function(sortState) — call when user clicks a column header to sort
// Drilldown
showDrilldown, // boolean — whether the drilldown panel is open
setShowDrilldown, // function(bool) — open/close the drilldown panel
onClick, // function({ filterValue, columnValue, columnName }) — trigger drilldown
states, // Lifecycle states array for the type
// Common (always present)
settings,
userData,
}
Example 1 — Simple report table with export
Display the data as a table and add an export button.
function SimpleReportWidget(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Box, ExportCSVButton, ShowDrillDownData } = protrakComponents;
const {
data,
config,
totalData,
filters,
instanceType,
sortState,
onSortApplied,
typePluralName,
states,
showDrilldown,
setShowDrilldown,
onClick,
} = pageContext;
const columns = config?.fields ?? [];
const cellStyle = { border: '1px solid #ddd', padding: '8px' };
return (
<Box direction="column">
<ExportCSVButton
data={data}
headers={columns}
filename={typePluralName + '-report'}
attributeFieldss={config.fields}
/>
{ShowDrillDownData(
showDrilldown,
totalData,
config,
filters,
instanceType,
setShowDrilldown,
sortState,
onSortApplied,
typePluralName,
states
)}
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} style={cellStyle}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map((col) => (
<td
key={col.key}
style={cellStyle}
onClick={() =>
onClick({
filterValue: row[config.group?.attribute?.attributeName],
columnName: col.key,
columnValue: row[col.key],
})
}
>
{row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</Box>
);
}
Example 2 — Color-banded grouped table
Add visual grouping with colored header bands for different data categories.
function GroupedReportWidget(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Box, Text, ExportCSVButton } = protrakComponents;
const { data, config, typePluralName } = pageContext;
const GRAND_TOTAL = 'Grand Total';
const groups = {};
data.forEach((row) => {
if (row.groupName !== GRAND_TOTAL) {
const g = row.category ?? 'Other';
if (!groups[g]) groups[g] = [];
groups[g].push(row);
}
});
const bandColors = [
'rgb(230,184,183)',
'rgb(204,192,218)',
'rgb(196,215,155)',
];
const cellStyle = { border: '1px solid #ddd', padding: '8px' };
return (
<Box direction="column" style={{ overflow: 'auto' }}>
<ExportCSVButton
data={data}
headers={config.fields}
filename={typePluralName}
attributeFieldss={config.fields}
/>
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
{Object.keys(groups).map((group, i) => (
<tr key={group}>
<th
colSpan={config.fields?.length ?? 1}
style={{
...cellStyle,
background: bandColors[i % bandColors.length],
}}
>
{group}
</th>
</tr>
))}
</thead>
<tbody>
{data
.filter((r) => r.groupName !== GRAND_TOTAL)
.map((row, i) => (
<tr key={i}>
{(config.fields ?? []).map((col) => (
<td key={col.key} style={cellStyle}>
{row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</Box>
);
}
Built-in helper components
ExportCSVButton
Adds a download button that exports the report data as a CSV file.
<ExportCSVButton
data={data} // Rows array
headers={columns} // Array of { key, label } objects
filename="my-report" // Downloaded file name (no .csv needed)
attributeFieldss={config.fields} // Pass config.fields from pageContext
/>
ShowDrillDownData
Call this function to render the built-in drilldown panel (slide-out with raw records).
{
ShowDrillDownData(
showDrilldown, // boolean
totalData, // raw records array
config, // report config
filters, // { columnName, columnValue, filterValue }
instanceType, // string
setShowDrilldown, // function to close
sortState, // { sortedBy, isDescending }
onSortApplied, // sort callback
typePluralName, // string
states // lifecycle states
);
}
Trigger the drilldown by calling onClick when the user clicks a cell:
onClick({
filterValue: row[config.group.attribute.attributeName],
columnName: 'TotalCost',
columnValue: row['TotalCost'],
});
Tips
- Always configure a custom program alongside the report widget — the program transforms raw data into the
dataarray your widget receives. datacontains the aggregated rows;totalDatacontains raw records. Usedatafor display,totalDatafor drilldown.- For deep-dive patterns, see Report Layout Pattern.