Dashboard: Data Fetch + Filter Table Pattern
Overview
The widget fetches its own data using useProtrakApi (or multiple calls), applies client-side filter dropdowns, and renders a table. There is no platform pre-aggregation — the widget is responsible for all data fetching and display. Optionally includes a CSV export button.
When to Use
- Widget needs to display cross-instance data that is not available from
totalDataoruseReportQuery - Data must be fetched from related instances, programs, or filtered queries
- Report-style view placed on a View layout tab (e.g., "Project Plan" tab on a Project instance)
- Dashboard widget that requires filtering by one or more dimensions
Applicable Layout Types
| Target | Notes |
|---|---|
DashboardLayout | Fetches its own data independently |
ViewLayout | Read-only report tab on an instance |
ReportLayout | Alternative to async report for simpler queries |
Pattern Structure
- Define API call configs with
React.useCallback— include any arguments they depend on - Call
useProtrakApifor each required data source - Show
<Spinner>while any required call is loading - Show
<ErrorModal>on error - On success, build display models (group, sort, aggregate as needed)
- Render filter
Dropdowncomponents and a filterable table - (Optional) Include an
ExportCSVButtonor clipboard copy button
Code Example — View Layout Report Tab
function ProjectPlanWidget(pageContext) {
const { protrakUtils, protrakComponents } =
React.useContext(customWidgetContext);
const { useProtrakApi } = protrakUtils;
const {
Container,
Spinner,
ErrorModal,
Dropdown,
Button,
ButtonEnums,
FormattedDate,
} = protrakComponents;
const [filterValue, setFilterValue] = React.useState('');
// Fetch related task instances
const getTasks = React.useCallback(
({ instanceId }) => ({
endpoint: `instances/${instanceId}/relatedInstances`,
config: {
method: 'GET',
params: {
'attributes[0]': 'Name',
'attributes[1]': 'TaskCategory',
'attributes[2]': 'Owner',
'attributes[3]': 'PlannedEffort',
'attributes[4]': 'TaskScheduledStartDate',
'attributes[5]': 'TaskScheduledEndDate',
'RelationFilters[0].InstanceId': instanceId,
'RelationFilters[0].TypeName': 'Task',
'RelationFilters[0].RelationTypeName': 'ProjectToTask',
'RelationFilters[0].RelationDirection': 'To',
getAllowedOperations: false,
getAllowedActions: false,
sortBy: 'Index',
IsSortByDescending: false,
skip: 0,
take: 10000,
},
},
}),
[]
);
// Fetch type definition for picklist options
const getTypeDetails = React.useCallback(
() => ({
endpoint: 'types/Task',
config: { method: 'GET' },
}),
[]
);
const { state: tasksResponse } = useProtrakApi({
requestConfig: getTasks,
instanceId: pageContext.instanceId,
});
const { state: typeResponse } = useProtrakApi({
requestConfig: getTypeDetails,
});
// Loading state
if (
tasksResponse.isLoading ||
!tasksResponse.data ||
typeResponse.isLoading ||
!typeResponse.data
) {
return (
<Container>
<Spinner />
</Container>
);
}
// Error states
if (tasksResponse.isError) {
return <ErrorModal message="Error loading tasks..." />;
}
if (typeResponse.isError) {
return <ErrorModal message="Error loading type data..." />;
}
// Build display model
const tasks = tasksResponse.data.items || [];
const categoryOptions =
typeResponse.data.attributes
?.find((a) => a.attributeName === 'TaskCategory')
?.options?.map((o) => ({ label: o.displayName, value: o.name })) || [];
const filtered = filterValue
? tasks.filter(
(t) =>
t.attributes?.find((a) => a.name === 'TaskCategory')
?.arrayValue?.[0] === filterValue
)
: tasks;
const tableStyle = {
borderCollapse: 'collapse',
width: '100%',
marginTop: '12px',
};
const headerStyle = {
border: '1px solid #dddddd',
padding: '8px',
backgroundColor: '#9cea9c',
textAlign: 'left',
fontSize: '13px',
};
const cellStyle = {
border: '1px solid #dddddd',
padding: '8px',
textAlign: 'left',
};
return (
<div style={{ margin: '10px' }}>
{/* Filter */}
<div style={{ marginBottom: '8px' }}>
<Dropdown
picklistOptions={[{ label: 'All', value: '' }, ...categoryOptions]}
defaultValues={filterValue}
onValueChange={(v) => setFilterValue(v?.value || '')}
/>
</div>
{/* Table */}
<table style={tableStyle}>
<thead>
<tr>
<th style={headerStyle}>Name</th>
<th style={headerStyle}>Category</th>
<th style={headerStyle}>Owner</th>
<th style={headerStyle}>Planned Start</th>
<th style={headerStyle}>Planned End</th>
</tr>
</thead>
<tbody>
{filtered.map((task, i) => {
const getAttr = (name) =>
task.attributes?.find((a) => a.name === name);
return (
<tr key={i}>
<td style={cellStyle}>{task.name}</td>
<td style={cellStyle}>
{getAttr('TaskCategory')?.arrayValue?.[0] || '-'}
</td>
<td style={cellStyle}>
{getAttr('Owner')?.userValues?.[0]?.userName || '-'}
</td>
<td style={cellStyle}>
<FormattedDate
value={getAttr('TaskScheduledStartDate')?.dateValue}
/>
</td>
<td style={cellStyle}>
<FormattedDate
value={getAttr('TaskScheduledEndDate')?.dateValue}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
Using useProtrakApi — Quick Reference
// Declarative — auto-executes when arguments are available (requestConfig variant)
const { state } = useProtrakApi({ requestConfig: myCallback, arg1, arg2 });
// state: { isLoading, isFulfilled, isError, data }
// Declarative — auto-executes using async promiseFn (receives protrakApiClient + authContext)
const { state } = useProtrakApi({
promiseFn: async ({ protrakApiClient, authContext }) => {
const res = await protrakApiClient(
'instances?instanceTypeName=MyType&skip=0&take=100',
{},
authContext
);
return res;
},
protrakApiClient,
});
// Deferred — manually triggered via run()
const { state, run } = useProtrakApi(
{
promiseFn: async ({ instanceId, authContext }) => {
return await protrakApiClient(`instances/${instanceId}`, {}, authContext);
},
},
{ defer: true }
);
run({ instanceId: 'abc123' }); // call when user triggers the action
// state.isFulfilled is true after run() completes
// Common endpoint patterns:
// GET instance: `instances/${id}`
// GET related instances: `instances/${id}/relatedInstances`
// GET type definition: `types/TypeName`
// POST program: `programs/ProgramName/executeCommonProgram`
// GET instances with filter: `instances?instanceTypeName=Type&skip=0&take=100`
promiseFn vs requestConfig:
requestConfig | promiseFn | |
|---|---|---|
| Style | Returns a config object { endpoint, config } | Async function — full control over API call |
| Auth | Handled internally | Must be called via protrakApiClient(url, {}, authContext) |
| Use when | Simple GET/POST with standard params | Complex multi-step calls, conditional logic inside the fetch |
| Deferred support | No | Yes — { defer: true } returns { state, run } |
Platform Utility Hooks
useRouter — Programmatic Navigation
Navigate to another page from within a widget:
const { useRouter } = protrakUtils;
const { history } = useRouter();
// Navigate to an instance view page, optionally specifying which widget tab to open
history.push(`/MyType/view/${instanceId}`, { widgetId: widgetTabId });
// Navigate to a list page
history.push(`/MyType/list`);
useDeleteInstance — In-Widget Instance Deletion
Provides a confirmation-flow delete that works inside a dialog:
const { useDeleteInstance } = protrakUtils;
const [deleteInstance, doDelete] = useDeleteInstance();
// deleteInstance: { isLoading, isFulfilled, isError, data }
// doDelete: function to trigger the delete
const handleDelete = () => {
doDelete({
promiseParams: {
instanceId: selectedInstanceId,
type: 'DeleteInstance',
},
});
};
// In the dialog footer:
if (deleteInstance.isFulfilled) {
// Show OK button
} else if (deleteInstance.isLoading) {
return <Spinner />;
} else if (deleteInstance.isError) {
// Show error message: deleteInstance.data?.response?.data
}
formatDateTimeWithTimeZone — Platform Date Formatting
Format a date string using the tenant's configured timezone and format:
const { formatDateTimeWithTimeZone } = protrakUtils;
// Get the tenant's date/time format from pageContext.settings
const { timeFormat, dateFormat, utcOffset } =
pageContext.settings.dateTimeFormat;
// Format a date string
const display = formatDateTimeWithTimeZone(
item.attributes.find((a) => a.name === 'Modified')?.dateValue,
timeFormat,
dateFormat,
utcOffset
);
// Returns formatted string like "14 May 2026 09:30 AM"
Notes on requestConfig Stability
requestConfig must be a stable function reference — always define it with React.useCallback:
// CORRECT — stable reference
const getConfig = React.useCallback(({ instanceId }) => ({
endpoint: `instances/${instanceId}`,
config: { method: 'GET', params: { 'attributes[0]': 'Name' } },
}), []);
// INCORRECT — new function on every render, causes infinite refetch loop
const getConfig = ({ instanceId }) => ({ ... });
Real Examples
ProjectPlanWidget.js— Protrak-ImplementationComponentVersionWidget.js,CustomizationComponentVersionWidget.js— Protrak-ImplementationSampleWiseLotScheduledData.js— GEECICategorywiseSchedule.js— Protrak-ImplementationActivityReport.js— Symmera (promiseFnauto-execute variant)SecurityModulesDashboardWidget.js— Symmera (promiseFn+useDeleteInstance+formatDateTimeWithTimeZone)DeviceCertificateRevokeHistoryCustomWidget.js— Symmera (promiseFndeferred variant)tenantConfiguration.js— Symmera (useRouternavigation)
Note:
CSADocumentReport.jsandLeadIndicatorReport.jslook similar but use the Report Layout Custom Visualization pattern — they receive pre-aggregateddata+configfrom the platform rather than fetching their own data.