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)