Skip to main content

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

TargetNotes
DashboardLayoutPrimary target — platform runs report before rendering
ReportLayoutAlso supported with the same totalData injection

Pattern Structure

  1. Destructure totalData from pageContext
  2. Define a parseRow(groupName) function that splits on '|' and maps each part to a named field
  3. Guard against empty totalData with a loading state
  4. 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 groupName pipe-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 totalData is undefined (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 options
  • response.data.attributes.find(a => a.attributeName === 'MyAttr').options — extract the options array
  • Each option has { displayName, name } — map to { label, value } for the Dropdown component
  • Filter happens in useEffect watching both pageContext and 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, MountMeru
  • BuildingProgressReportCustomWidget.js — Aaryan Devcon, C360, MountMeru
  • WeeklyPlannedVsActualCustomeWidget.js — Aaryan Devcon
  • InventoryMaterialConsumedVsPlannedCustomWidget.js — MountMeru (note: also uses data/onClick from Report Layout)
  • TodayProcessesDetails.js — GEECI (hybrid: totalData + useProtrakApi for equipment names)
  • MultiLotSamplesReportByProcessFlow.js — GEECI (pure totalData, complex multi-dimension visualization)
  • TypicalElementSummaryReport.js — Buildcast, DevinciPrecastPOC
  • ComponentOperatingSystemWidget.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)