diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx
index 44f2900f5e..ede75f9963 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx
@@ -434,28 +434,42 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
);
}
+ const eventPromise = getJobModel(job.type).readEvents(job.id, {
+ ...params,
+ ...parseQueryString(QS_CONFIG, location.search),
+ });
+
+ let countRequest;
+ if (isJobRunning(job?.status)) {
+ // If the job is running, it means we're using limit-offset pagination. Requests
+ // with limit-offset pagination won't return a total event count for performance
+ // reasons. In this situation, we derive the remote row count by using the highest
+ // counter available in the database.
+ countRequest = async () => {
+ const {
+ data: { results: lastEvents = [] },
+ } = await getJobModel(job.type).readEvents(job.id, {
+ order_by: '-counter',
+ limit: 1,
+ });
+ return lastEvents.length >= 1 ? lastEvents[0].counter : 0;
+ };
+ } else {
+ countRequest = async () => {
+ const {
+ data: { count: eventCount },
+ } = await eventPromise;
+ return eventCount;
+ };
+ }
+
try {
const [
{
data: { results: fetchedEvents = [] },
},
- {
- data: { results: lastEvents = [] },
- },
- ] = await Promise.all([
- getJobModel(job.type).readEvents(job.id, {
- ...params,
- ...parseQueryString(QS_CONFIG, location.search),
- }),
- getJobModel(job.type).readEvents(job.id, {
- order_by: '-counter',
- limit: 1,
- }),
- ]);
- let count = 0;
- if (lastEvents.length >= 1 && lastEvents[0]?.counter) {
- count = lastEvents[0]?.counter;
- }
+ count,
+ ] = await Promise.all([eventPromise, countRequest()]);
if (isMounted.current) {
let countOffset = 0;
diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
index 5b9928b4e3..76f419a16b 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
@@ -13,6 +13,22 @@ import mockFilteredJobEventsData from './data.filtered_job_events.json';
jest.mock('../../../api');
+const applyJobEventMock = mockJobEvents => {
+ const mockReadEvents = async (jobId, params) => {
+ const [...results] = mockJobEvents.results;
+ if (params.order_by && params.order_by.includes('-')) {
+ results.reverse();
+ }
+ return {
+ data: {
+ results,
+ count: mockJobEvents.count,
+ },
+ };
+ };
+ JobsAPI.readEvents = jest.fn().mockImplementation(mockReadEvents);
+};
+
const generateChattyRows = () => {
const rows = [
'',
@@ -82,24 +98,13 @@ const originalOffsetWidth = Object.getOwnPropertyDescriptor(
describe('', () => {
let wrapper;
const mockJob = mockJobData;
- const mockJobEvents = mockJobEventsData;
+
beforeEach(() => {
- JobsAPI.readEvents = (jobId, params) => {
- const [...results] = mockJobEvents.results;
- if (params.order_by && params.order_by.includes('-')) {
- results.reverse();
- }
- return {
- data: {
- results,
- },
- };
- };
+ applyJobEventMock(mockJobEventsData);
});
afterEach(() => {
jest.clearAllMocks();
- wrapper.unmount();
});
test('initially renders successfully', async () => {
@@ -141,7 +146,7 @@ describe('', () => {
});
wrapper.update();
jobEvents = wrapper.find('JobEvent');
- expect(jobEvents.at(jobEvents.length - 1).prop('stdout')).toBe(
+ expect(jobEvents.at(jobEvents.length - 2).prop('stdout')).toBe(
'\r\nPLAY RECAP *********************************************************************\r\n\u001b[0;32mlocalhost\u001b[0m : \u001b[0;32mok=1 \u001b[0m changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 \r\n'
);
await act(async () => {
@@ -149,10 +154,10 @@ describe('', () => {
});
wrapper.update();
jobEvents = wrapper.find('JobEvent');
- expect(jobEvents.at(1).prop('stdout')).toBe(
+ expect(jobEvents.at(0).prop('stdout')).toBe(
'\u001b[0;32mok: [localhost] => (item=76) => {\u001b[0m\r\n\u001b[0;32m "msg": "This is a debug message: 76"\u001b[0m\r\n\u001b[0;32m}\u001b[0m'
);
- expect(jobEvents.at(2).prop('stdout')).toBe(
+ expect(jobEvents.at(1).prop('stdout')).toBe(
'\u001b[0;32mok: [localhost] => (item=77) => {\u001b[0m\r\n\u001b[0;32m "msg": "This is a debug message: 77"\u001b[0m\r\n\u001b[0;32m}\u001b[0m'
);
await act(async () => {
@@ -169,7 +174,7 @@ describe('', () => {
});
wrapper.update();
jobEvents = wrapper.find('JobEvent');
- expect(jobEvents.at(jobEvents.length - 1).prop('stdout')).toBe(
+ expect(jobEvents.at(jobEvents.length - 2).prop('stdout')).toBe(
'\r\nPLAY RECAP *********************************************************************\r\n\u001b[0;32mlocalhost\u001b[0m : \u001b[0;32mok=1 \u001b[0m changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 \r\n'
);
Object.defineProperty(
@@ -266,11 +271,7 @@ describe('', () => {
wrapper = mountWithContexts();
});
await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
- JobsAPI.readEvents = jest.fn();
- JobsAPI.readEvents.mockClear();
- JobsAPI.readEvents.mockResolvedValueOnce({
- data: mockFilteredJobEventsData,
- });
+ applyJobEventMock(mockFilteredJobEventsData);
await act(async () => {
wrapper.find(searchTextInput).instance().value = '99';
wrapper.find(searchTextInput).simulate('change');
@@ -281,14 +282,13 @@ describe('', () => {
});
wrapper.update();
expect(JobsAPI.readEvents).toHaveBeenCalled();
- // TODO: Fix these assertions
- // const jobEvents = wrapper.find('JobEvent');
- // expect(jobEvents.at(0).prop('stdout')).toBe(
- // '\u001b[0;32mok: [localhost] => (item=99) => {\u001b[0m\r\n\u001b[0;32m "msg": "This is a debug message: 99"\u001b[0m\r\n\u001b[0;32m}\u001b[0m'
- // );
- // expect(jobEvents.at(1).prop('stdout')).toBe(
- // '\u001b[0;32mok: [localhost] => (item=199) => {\u001b[0m\r\n\u001b[0;32m "msg": "This is a debug message: 199"\u001b[0m\r\n\u001b[0;32m}\u001b[0m'
- // );
+ const jobEvents = wrapper.find('JobEvent');
+ expect(jobEvents.at(0).prop('stdout')).toBe(
+ '\u001b[0;32mok: [localhost] => (item=99) => {\u001b[0m\r\n\u001b[0;32m "msg": "This is a debug message: 99"\u001b[0m\r\n\u001b[0;32m}\u001b[0m'
+ );
+ expect(jobEvents.at(1).prop('stdout')).toBe(
+ '\u001b[0;32mok: [localhost] => (item=199) => {\u001b[0m\r\n\u001b[0;32m "msg": "This is a debug message: 199"\u001b[0m\r\n\u001b[0;32m}\u001b[0m'
+ );
});
test('should throw error', async () => {