Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions force-app/main/default/classes/timeline/AsyncJobTimelineChains.cls
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,24 @@ public with sharing class AsyncJobTimelineChains {
return chainIds;
}

public void refreshFrom(List<AsyncResult__c> rows) {
Map<String, ChainOrder> chainById = new Map<String, ChainOrder>();
for (ChainOrder chain : chains) {
chainById.put(chain.chainId, chain);
}
Set<String> refreshed = new Set<String>();
for (AsyncResult__c row : rows) {
ChainOrder chain = chainById.get(row.ChainId__c);
if (chain == null) {
continue;
}
if (refreshed.add(chain.chainId)) {
chain.status = STATUS_COMPLETED;
}
chain.absorb(chainStatusFor(row.Status__c));
}
}

public Map<String, Integer> counts() {
Map<String, Integer> counts = new Map<String, Integer>{
'all' => chains.size(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,37 @@ private class AsyncJobTimelineControllerTest {
Assert.areEqual(new List<String>{ 'done' }, chainIdsOf(onlyDone));
}

@IsTest
private static void shouldCountAPageChainByTheRowsReadForThePage() {
Account order = new Account(Name = 'Order');
insert order;
List<AsyncResult__c> live = seedChain(
order,
'live',
'Live run',
2,
System.now().addHours(-1)
);
live[1].Status__c = QueueableManager.STATUS_ENQUEUED;
update live[1];
Account otherOrder = new Account(Name = 'Other order');
insert otherOrder;
seedChain(otherOrder, 'elsewhere', 'Elsewhere run', 1, System.now().addHours(-1));
AsyncJobTimelineChains chains = new AsyncJobTimelineChains(order.Id, 7);
live[1].Status__c = QueueableManager.STATUS_COMPLETED;
update live[1];

chains.refreshFrom([SELECT ChainId__c, Status__c FROM AsyncResult__c]);

Assert.areEqual(
0,
chains.counts().get('inflight'),
'The step finished after the snapshot.'
);
Assert.areEqual(1, chains.counts().get('completed'));
Assert.areEqual(1, chains.counts().get('all'), 'Rows of another record are ignored.');
}

@IsTest
private static void shouldLinkARequeuedRowToItsReplay() {
Account order = new Account(Name = 'Order');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,27 @@ public with sharing class AsyncJobTimelineLoader {
AsyncJobTimelineController.TimelinePage page = new AsyncJobTimelineController.TimelinePage();
page.hasMore = chainIds.size() > chainOffset + chainLimit;
page.totalChainCount = chainIds.size();
page.fingerprint = fingerprintOf(recordId, pageChainIds);
List<AsyncResult__c> rows = rowsOf(pageChainIds);
chains.refreshFrom(rows);
page.counts = chains.counts();
page.olderChainCount = chains.olderCount();
page.sequences = sequencesOf(pageChainIds);
page.fingerprint = fingerprintOf(recordId, pageChainIds);
page.sequences = sequencesOf(pageChainIds, rows);
page.loadedAt = System.now();
return page;
}

private static List<AsyncJobTimelineController.Sequence> sequencesOf(List<String> chainIds) {
private static List<AsyncJobTimelineController.Sequence> sequencesOf(
List<String> chainIds,
List<AsyncResult__c> rows
) {
Map<String, AsyncJobTimelineController.Sequence> byChain = new Map<String, AsyncJobTimelineController.Sequence>();
for (String chainId : chainIds) {
byChain.put(chainId, new AsyncJobTimelineController.Sequence(chainId));
}
if (chainIds.isEmpty()) {
return byChain.values();
}
List<AsyncResult__c> rows = rowsOf(chainIds);
Map<String, String> apexJobStatusById = apexJobStatusesFor(enqueuedJobIdsOf(rows));
Map<Id, String> nameById = AsyncJobTimelineNames.of(linkedRecordIdsOf(rows));
Map<Id, AsyncResult__c> replayBySourceId = replaysOf(rows);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
<description>Every async job that ran, runs or will run for this record, with requeue for failed steps.</description>
<targets>
<target>lightning__RecordPage</target>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
Expand Down Expand Up @@ -49,41 +47,5 @@
default="false"
description="When on, a chain that enters in flight opens by itself and the others collapse, for a card left open on a screen. Off: the new chain is only flagged. The viewer can switch it for the session."/>
</targetConfig>
<targetConfig targets="lightningCommunity__Default">
<property
name="recordId"
type="String"
label="Record Id"
default="{!recordId}"
description="Leave as {!recordId} on a record page."/>
<property name="cardTitle" type="String" label="Card title" default="Async Jobs"/>
<property
name="daysBack"
type="Integer"
label="Default range in days"
default="7"
min="1"
max="365"/>
<property
name="chainsPerPage"
type="Integer"
label="Sequences per page"
default="0"
min="0"
max="50"/>
<property
name="maxHeight"
type="Integer"
label="Max height in pixels"
default="600"
min="0"
max="4000"/>
<property
name="liveRefreshOff"
type="Boolean"
label="Turn off live refresh"
default="false"/>
<property name="followNewRuns" type="Boolean" label="Follow new runs" default="false"/>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
2 changes: 1 addition & 1 deletion release-notes/v3.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Finalizers of the current job still run first. Closes #68.
requeue, and flags an idle card that has not checked for ten minutes.
- The card has a range menu, counts and filters chains across the whole range, pages in memory
through blocks of 50 chains, scrolls inside a configurable height, asks before a requeue, opens
linked records and the replay of a requeued step, and targets Experience Cloud record pages.
linked records and the replay of a requeued step.
- The chain map is a pipeline of nodes with elbow connectors for dependencies, opened on the
first failed node.
- The card keeps a fixed height, fits its page size to it, refreshes in place, opens one chain by
Expand Down
3 changes: 2 additions & 1 deletion website/ai-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ In tests, inject them: `AsyncMock.jobSettings(new List<QueueableJobSetting__mdt>
One row per job, written after its last attempt, when `CreateResult__c` is on, a payload is
stored, or the job is linked to a record; written at enqueue instead when `TrackJobProgress__c` is
`Yes`. A batch writes one row per failed scope and nothing for a successful one. Read access through
the `AsyncResultAccess` permission set, which also covers `AsyncResultLink__c` (one row per result
the `AsyncResultAccess` permission set (View All: every result in the org, the record page card is
not a sharing boundary), which also covers `AsyncResultLink__c` (one row per result
and linked record: `Key__c` indexed, `Source__c` `Salesforce` or whatever `linkExternalRecord`
was given, `ObjectName__c`). The requeue actions on the record page need the
`AsyncRequeue` custom permission from the `AsyncRequeueAccess` permission set.
Expand Down
4 changes: 3 additions & 1 deletion website/features/job-timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ A chunk run that still misses pages but has been quiet for ten minutes counts as

## The record page component

`Async Jobs` targets any record page, in Lightning and in Aura-based Experience sites. One card per
`Async Jobs` targets any Lightning record page. One card per
chain, newest first by the time the chain was created. A chain never moves when it finishes, so the
row you are watching stays where it is, and only a new run shifts the list. The `In flight` chip turns
blue with a pulsing dot while something runs: click it to see only those chains. A chain that finishes
Expand Down Expand Up @@ -232,6 +232,8 @@ What the card refuses to render without a way out:

The viewer needs read on `AsyncResult__c` and `AsyncResultLink__c` plus access to the controller
class. The `AsyncResultAccess` permission set grants all of it. Without it the card says so by name.
It is read on every result in the org, with View All, so the card is not a sharing boundary: give
it to people who may see every job, not only the ones for the record in front of them.

## Requeue from the timeline

Expand Down
4 changes: 2 additions & 2 deletions website/introduction/packaged-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ actions on the `Async Jobs` record page component. Assign it to operators only.

### 6. Place the record page component

`Async Jobs` is in App Builder under custom components, for Lightning record pages and Aura-based
Experience site record pages. Drop it on any record page; the viewer needs
`Async Jobs` is in App Builder under custom components, for Lightning record pages. Drop it on any
record page; the viewer needs
`btcdev__AsyncResultAccess`. See [Job Timeline](/features/job-timeline).

## Feature by feature
Expand Down
Loading