Skip to main content

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 totalData or useReportQuery
  • 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

TargetNotes
DashboardLayoutFetches its own data independently
ViewLayoutRead-only report tab on an instance
ReportLayoutAlternative to async report for simpler queries

Pattern Structure

  1. Define API call configs with React.useCallback — include any arguments they depend on
  2. Call useProtrakApi for each required data source
  3. Show <Spinner> while any required call is loading
  4. Show <ErrorModal> on error
  5. On success, build display models (group, sort, aggregate as needed)
  6. Render filter Dropdown components and a filterable table
  7. (Optional) Include an ExportCSVButton or 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:

requestConfigpromiseFn
StyleReturns a config object { endpoint, config }Async function — full control over API call
AuthHandled internallyMust be called via protrakApiClient(url, {}, authContext)
Use whenSimple GET/POST with standard paramsComplex multi-step calls, conditional logic inside the fetch
Deferred supportNoYes — { 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-Implementation
  • ComponentVersionWidget.js, CustomizationComponentVersionWidget.js — Protrak-Implementation
  • SampleWiseLotScheduledData.js — GEECI
  • CategorywiseSchedule.js — Protrak-Implementation
  • ActivityReport.js — Symmera (promiseFn auto-execute variant)
  • SecurityModulesDashboardWidget.js — Symmera (promiseFn + useDeleteInstance + formatDateTimeWithTimeZone)
  • DeviceCertificateRevokeHistoryCustomWidget.js — Symmera (promiseFn deferred variant)
  • tenantConfiguration.js — Symmera (useRouter navigation)

Note: CSADocumentReport.js and LeadIndicatorReport.js look similar but use the Report Layout Custom Visualization pattern — they receive pre-aggregated data + config from the platform rather than fetching their own data.