Skip to main content

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:

  1. Runs a custom program (C# server-side code) to process and aggregate the raw data
  2. Passes the result to your custom widget as pageContext.data
  3. 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 data array your widget receives.
  • data contains the aggregated rows; totalData contains raw records. Use data for display, totalData for drilldown.
  • For deep-dive patterns, see Report Layout Pattern.