Dashboard: totalData + Pipe Parsing Pattern
Overview
The platform pre-aggregates report data and passes it to the widget as pageContext.totalData. Each row contains a groupName string with pipe-delimited (|) values representing different columns. The widget parses these strings and renders them as a table or chart.
This is the simplest dashboard pattern — the platform does all the data work; the widget only handles display.
When to Use
- The report query is configured in the Protrak admin and executed by the platform before rendering
- Data is pre-grouped and delivered as pipe-delimited strings in
groupName - Widget needs to display a static or filterable table of aggregated data
Applicable Layout Types
| Target | Notes |
|---|---|
DashboardLayout | Primary target — platform runs report before rendering |
ReportLayout | Also supported with the same totalData injection |
Pattern Structure
- Destructure
totalDatafrompageContext - Define a
parseRow(groupName)function that splits on'|'and maps each part to a named field - Guard against empty
totalDatawith a loading state - Render the parsed rows in a table
Code Example
function BuildingProgressReportWidget(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Container, Spinner } = protrakComponents;
const { totalData } = pageContext;
// Each row: { groupName: "BuildingA|InProgress|45|On track", ... }
const parseRow = (groupName) => {
const parts = (groupName || '').split('|');
return {
building: parts[0] || '-',
status: parts[1] || '-',
progress: parseInt(parts[2]) || 0,
remarks: parts[3] || '-',
};
};
if (!totalData || totalData.length === 0) {
return (
<Container>
<Spinner />
</Container>
);
}
const rows = totalData.map((d) => parseRow(d.groupName));
const tableStyle = { borderCollapse: 'collapse', width: '100%' };
const headerStyle = {
border: '1px solid #dddddd',
padding: '8px',
backgroundColor: '#f9f9f9',
textAlign: 'left',
fontWeight: 'bold',
};
const cellStyle = {
border: '1px solid #dddddd',
padding: '8px',
textAlign: 'left',
};
return (
<div style={{ boxSizing: 'border-box', margin: '12px' }}>
<table style={tableStyle}>
<thead>
<tr>
<th style={headerStyle}>Building</th>
<th style={headerStyle}>Status</th>
<th style={headerStyle}>Progress (%)</th>
<th style={headerStyle}>Remarks</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
<td style={cellStyle}>{row.building}</td>
<td style={cellStyle}>{row.status}</td>
<td style={cellStyle}>{row.progress}</td>
<td style={cellStyle}>{row.remarks}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Hybrid Variant: totalData + useProtrakApi
Some widgets receive totalData from the platform but also need additional API calls to enrich the display (e.g., fetching dimension names for column headers). This hybrid approach combines totalData with a useProtrakApi call:
function HybridDashboardWidget(pageContext) {
const { protrakUtils, protrakComponents } =
React.useContext(customWidgetContext);
const { useProtrakApi } = protrakUtils;
const { Container, Spinner } = protrakComponents;
const { totalData } = pageContext;
const [dimensionNames, setDimensionNames] = React.useState([]);
const fetchDimensions = React.useCallback(
() => ({
endpoint: 'programs/GetDimensionNames/executeCommonProgram',
config: { method: 'POST', data: [] },
}),
[]
);
const { state: dimResponse } = useProtrakApi({
requestConfig: fetchDimensions,
});
React.useEffect(() => {
if (dimResponse.isFulfilled && dimResponse.data) {
// Only include dimensions present in totalData
const present = new Set((totalData || []).map((r) => r['dimensionKey']));
setDimensionNames(dimResponse.data.filter((n) => present.has(n)));
}
}, [dimResponse, totalData]);
if (!dimResponse.isFulfilled) {
return (
<Container>
<Spinner />
</Container>
);
}
return (
<table>
{/* Render cross-tab using totalData rows + dimensionNames as column headers */}
</table>
);
}
Notes
- The
groupNamepipe-delimited format is a platform convention — the report query configuration controls the order of values. Always document the expected column order in a comment at the top of the widget. - If
totalDataisundefined(widget rendered outside a report context), default to an empty array:const rows = (totalData || []).map(...).
Variant: Fetch Picklist Options from Type Definition
When filter dropdowns need their options sourced from a type's attribute definition (not hardcoded), use the types/{typeName} endpoint.
function ComponentOSWidget(pageContext) {
const { protrakUtils, protrakComponents } =
React.useContext(customWidgetContext);
const { useProtrakApi } = protrakUtils;
const { Dropdown, ExportCSVButton } = protrakComponents;
const [envTypeOptions, setEnvTypeOptions] = React.useState([]);
const [envFilter, setEnvFilter] = React.useState({
label: 'Production',
value: 'Production',
});
const [tableData, setTableData] = React.useState([]);
// Fetch picklist options from the type definition
const getTypeConfig = React.useCallback(
({ typeName }) => ({
endpoint: 'types/' + typeName,
config: { method: 'GET' },
}),
[]
);
const { state: envTypeResponse } = useProtrakApi({
requestConfig: getTypeConfig,
typeName: 'Environment',
});
React.useEffect(() => {
if (envTypeResponse.isFulfilled && envTypeResponse.data?.attributes) {
const options =
envTypeResponse.data.attributes.find(
(a) => a.attributeName === 'TypeOfEnvironment'
)?.options || [];
setEnvTypeOptions(
options.map((o) => ({ label: o.displayName, value: o.name }))
);
}
}, [envTypeResponse]);
// Filter totalData in memory when dropdown changes
React.useEffect(() => {
if (pageContext?.totalData) {
const filtered = pageContext.totalData.filter(
(d) => d.environmentType === envFilter.label
);
setTableData(filtered);
}
}, [pageContext, envFilter]);
// CSV export headers
const csvHeaders = [
{ key: 'customerName', attributeType: 'Text' },
{ key: 'environmentType', attributeType: 'Text' },
];
return (
<div>
<Dropdown
defaultValues={envFilter}
picklistOptions={envTypeOptions}
isMultiselect={false}
onValueChange={setEnvFilter}
/>
<ExportCSVButton
data={tableData}
headers={csvHeaders}
filename="EnvironmentReport"
/>
{/* render tableData */}
</div>
);
}
Key points:
endpoint: 'types/' + typeName— fetches the full type definition including all attribute schemas and picklist optionsresponse.data.attributes.find(a => a.attributeName === 'MyAttr').options— extract the options array- Each option has
{ displayName, name }— map to{ label, value }for theDropdowncomponent - Filter happens in
useEffectwatching bothpageContextand the selected filter value
ExportCSVButton
The ExportCSVButton component provides one-click CSV download for any table data:
const { ExportCSVButton } = protrakComponents;
<ExportCSVButton
data={tableData} // Array of row objects to export
headers={[
// Column config
{ key: 'name', attributeType: 'Text' },
{ key: 'count', attributeType: 'Numeric' },
]}
filename="MyReport" // Downloaded file name (no extension needed)
/>;
Real Examples
BuildingCriticalDelayReportCustomWidget.js— Aaryan Devcon, C360, MountMeruBuildingProgressReportCustomWidget.js— Aaryan Devcon, C360, MountMeruWeeklyPlannedVsActualCustomeWidget.js— Aaryan DevconInventoryMaterialConsumedVsPlannedCustomWidget.js— MountMeru (note: also usesdata/onClickfrom Report Layout)TodayProcessesDetails.js— GEECI (hybrid:totalData+useProtrakApifor equipment names)MultiLotSamplesReportByProcessFlow.js— GEECI (puretotalData, complex multi-dimension visualization)TypicalElementSummaryReport.js— Buildcast, DevinciPrecastPOCComponentOperatingSystemWidget.js— Protrak-Implementation (types/ picklist variant)ComponentOperatingSystemWidgetOSWise.js— Protrak-Implementation (types/ picklist variant, OS-grouped view)CustomersWithRelatedEnvWithoutServerDetailsInstancesWidget.js— Protrak-Implementation (types/ picklist variant)CustomizationComponentVersionWidget.js— Protrak-Implementation (types/ picklist + text filter)