Skip to main content

Dashboard: Async Report (useReportQuery) Pattern

Overview

Some report layouts run long-running queries asynchronously on the server. The platform provides a useReportQuery global hook that polls for completion. The widget waits for the Completed status before rendering data, and shows an error on Failed.

When to Use

  • The report is configured as an async report in the Protrak admin (not a simple pre-aggregated totalData report)
  • Report data arrives as a structured JSON object (reportJson.data) rather than a totalData array
  • Widget needs to handle a loading state while the platform processes the report

Applicable Layout Types

TargetNotes
DashboardLayoutPrimary target — platform injects useReportQuery and a config variable
ReportLayoutAlso supported

Pattern Structure

  1. Call useReportQuery(config, 'TypeName')config is a global variable injected by the platform's report configuration (not defined in the widget)
  2. Track report status via reportState.data.status in a useEffect
  3. Render a Spinner while status !== 'Completed'
  4. Render an ErrorModal on status === 'Failed'
  5. On Completed, access data via reportState.data.reportJson.data

Code Example

// NOTE: 'config' is a global variable injected by the report layout configuration.
// It is NOT defined inside the widget function.
// NOTE: 'useReportQuery' is a global hook injected by the platform for report layouts.

function AnyWidget(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Container, Spinner, ErrorModal } = protrakComponents;

const [reportStatus, setReportStatus] = React.useState(null);

// useReportQuery polls the server until the report completes
const { reportState } = useReportQuery(config, 'Element');

React.useEffect(() => {
if (reportState.data) {
setReportStatus(reportState.data.status);
}
}, [reportState.data]);

if (reportStatus === 'Completed') {
const rawData = reportState.data?.reportJson?.data || [];

if (rawData.length === 0) {
return (
<Container>
<p>No data available.</p>
</Container>
);
}

return <RenderTable data={processData(rawData)} />;
}

if (reportStatus === 'Failed') {
return (
<Container>
<ErrorModal message="Error while fetching report data..." />
</Container>
);
}

// Still loading (status is null, 'Pending', or 'Processing')
return (
<Container>
<Spinner />
</Container>
);
}

// Helper: transform raw report rows into display-ready data
function processData(rawData) {
// Apply any grouping, aggregation, or transformation here
return rawData.reduce((acc, row) => {
// ... aggregate logic ...
return acc;
}, {});
}

// Sub-component: renders the processed data
const RenderTable = ({ data }) => {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Dropdown } = protrakComponents;

const [filter, setFilter] = React.useState(null);

const rows = Object.values(data);
const filtered = filter ? rows.filter((r) => r.category === filter) : rows;

return (
<div>
<Dropdown
picklistOptions={[
...new Set(
rows.map((r) => ({ label: r.category, value: r.category }))
),
]}
onValueChange={setFilter}
/>
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
{/* ... render filtered rows ... */}
</table>
</div>
);
};

Key Differences vs totalData Pattern

AspecttotalData PatternuseReportQuery Pattern
Data sourcepageContext.totalDatareportState.data.reportJson.data
Loading stateNo loading — data arrives synchronouslyMust handle loading/failed states
Report configurationDeclarative in adminAsync report config; config variable injected
Filter dropdownsWidget manages filtersWidget manages filters
Sub-componentsOptionalCommon — separate RenderTable component

Notes

  • useReportQuery and config are globally injected by the platform's report layout infrastructure. Do not attempt to define or import them.
  • Sub-components (like RenderTable) defined inside the widget's .js file are fine — they are within the function scope and do not pollute the global scope. Alternatively, define them as inner functions.
  • Avoid defining variables like elementTypes, levels, grandTotal at module scope — use useRef or useMemo inside the component instead.

Real Examples

  • AnyWidget.js — Buildcast, DevinciPrecastPOC
  • PlannedVsActualDailyReportWidget.js — Buildcast, DevinciPrecastPOC
  • PlannedVsActualDailyReportForPrestressedTypeElements.js — Buildcast, DevinciPrecastPOC
  • PlannedVsActualDailyReportForNotPrestressedTypeElements.js — Buildcast, DevinciPrecastPOC
  • ProjectSummaryReport.js — Buildcast, DevinciPrecastPOC