diff --git a/awx/ui/src/api/models/Jobs.js b/awx/ui/src/api/models/Jobs.js index 8a064e6251..2e12c3ceb3 100644 --- a/awx/ui/src/api/models/Jobs.js +++ b/awx/ui/src/api/models/Jobs.js @@ -19,6 +19,10 @@ class Jobs extends RunnableMixin(Base) { readDetail(id) { return this.http.get(`${this.baseUrl}${id}/`); } + + readChildrenSummary(id) { + return this.http.get(`${this.baseUrl}${id}/job_events/children_summary/`); + } } export default Jobs; diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index c5eb8ca217..87f900aee1 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -18,7 +18,7 @@ import ContentError from 'components/ContentError'; import ContentLoading from 'components/ContentLoading'; import ErrorDetail from 'components/ErrorDetail'; import StatusLabel from 'components/StatusLabel'; -import { JobEventsAPI } from 'api'; +import { JobsAPI } from 'api'; import { getJobModel, isJobRunning } from 'util/jobs'; import useRequest, { useDismissableError } from 'hooks/useRequest'; @@ -99,8 +99,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { const scrollHeight = useRef(0); const history = useHistory(); const eventByUuidRequests = useRef([]); - const siblingRequests = useRef([]); - const numEventsRequests = useRef([]); const fetchEventByUuid = async (uuid) => { let promise = eventByUuidRequests.current[uuid]; @@ -113,60 +111,15 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { return data.results[0] || null; }; - const fetchNextSibling = async (parentEventId, counter) => { - const key = `${parentEventId}-${counter}`; - let promise = siblingRequests.current[key]; - if (!promise) { - promise = JobEventsAPI.readChildren(parentEventId, { - page_size: 1, - order_by: 'counter', - counter__gt: counter, - }); - siblingRequests.current[key] = promise; - } - - const { data } = await promise; - siblingRequests.current[key] = null; - return data.results[0] || null; - }; - - const fetchNextRootNode = async (counter) => { - const { data } = await getJobModel(job.type).readEvents(job.id, { - page_size: 1, - order_by: 'counter', - counter__gt: counter, - parent_uuid: '', - }); - return data.results[0] || null; - }; - - const fetchNumEvents = async (startCounter, endCounter) => { - if (endCounter <= startCounter + 1) { - return 0; - } - const key = `${startCounter}-${endCounter}`; - let promise = numEventsRequests.current[key]; - if (!promise) { - const params = { - page_size: 1, - order_by: 'counter', - counter__gt: startCounter, - }; - if (endCounter) { - params.counter__lt = endCounter; - } - promise = getJobModel(job.type).readEvents(job.id, params); - numEventsRequests.current[key] = promise; - } - - const { data } = await promise; - numEventsRequests.current[key] = null; - return data.count || 0; - }; + const fetchChildrenSummary = () => JobsAPI.readChildrenSummary(job.id); const [jobStatus, setJobStatus] = useState(job.status ?? 'waiting'); + const [forceFlatMode, setForceFlatMode] = useState(false); const isFlatMode = isJobRunning(jobStatus) || location.search.length > 1; + const [isTreeReady, setIsTreeReady] = useState(false); + const [onReadyEvents, setOnReadyEvents] = useState([]); + const { addEvents, toggleNodeIsCollapsed, @@ -181,11 +134,12 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { } = useJobEvents( { fetchEventByUuid, - fetchNextSibling, - fetchNextRootNode, - fetchNumEvents, + fetchChildrenSummary, + setForceFlatMode, + setJobTreeReady: () => setIsTreeReady(true), }, - isFlatMode + job.id, + isFlatMode || forceFlatMode ); const [wsEvents, setWsEvents] = useState([]); const [cssMap, setCssMap] = useState({}); @@ -203,6 +157,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { const [isMonitoringWebsocket, setIsMonitoringWebsocket] = useState(false); const [lastScrollPosition, setLastScrollPosition] = useState(0); + useEffect(() => { + if (!isTreeReady || !onReadyEvents.length) { + return; + } + addEvents(onReadyEvents); + setOnReadyEvents([]); + }, [isTreeReady, onReadyEvents]); // eslint-disable-line react-hooks/exhaustive-deps + const totalNonCollapsedRows = Math.max( remoteRowCount - getNumCollapsedEvents(), 0 @@ -216,13 +178,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { ); useEffect(() => { - const pendingRequests = [ - ...Object.values(eventByUuidRequests.current || {}), - ...Object.values(siblingRequests.current || {}), - ...Object.values(numEventsRequests.current || {}), - ]; + const pendingRequests = Object.values(eventByUuidRequests.current || {}); setHasContentLoading(true); // prevents "no content found" screen from flashing - Promise.all(pendingRequests).then(() => { + Promise.allSettled(pendingRequests).then(() => { setRemoteRowCount(0); clearLoadedEvents(); loadJobEvents(); @@ -412,7 +370,11 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { ...newCssMap, })); const lastCounter = events[events.length - 1]?.counter || 50; - addEvents(events); + if (isTreeReady) { + addEvents(events); + } else { + setOnReadyEvents((prev) => prev.concat(events)); + } setHighestLoadedCounter(lastCounter); setRemoteRowCount(count + countOffset); } catch (err) { @@ -707,7 +669,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { onScrollNext={handleScrollNext} onScrollPrevious={handleScrollPrevious} toggleExpandCollapseAll={handleExpandCollapseAll} - isFlatMode={isFlatMode} + isFlatMode={isFlatMode || forceFlatMode} isTemplateJob={job.type === 'job'} isAllCollapsed={isAllCollapsed} /> diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.test.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.test.js index 03797f1ef8..19af8a76d6 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.test.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.test.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len */ import React from 'react'; import { act } from 'react-dom/test-utils'; import { JobsAPI, JobEventsAPI } from 'api'; @@ -26,14 +25,9 @@ const applyJobEventMock = (mockJobEvents) => { }; }; JobsAPI.readEvents = jest.fn().mockImplementation(mockReadEvents); - JobEventsAPI.readChildren = jest.fn().mockResolvedValue({ + JobsAPI.readChildrenSummary = jest.fn().mockResolvedValue({ data: { - results: [ - { - counter: 20, - uuid: 'abc-020', - }, - ], + 1: [0, 100], }, }); }; diff --git a/awx/ui/src/screens/Job/JobOutput/shared/JobEventLine.js b/awx/ui/src/screens/Job/JobOutput/shared/JobEventLine.js index db399e908e..a227cdfad0 100644 --- a/awx/ui/src/screens/Job/JobOutput/shared/JobEventLine.js +++ b/awx/ui/src/screens/Job/JobOutput/shared/JobEventLine.js @@ -4,7 +4,6 @@ export default styled.div` display: flex; &:hover { - background-color: white; cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')}; } diff --git a/awx/ui/src/screens/Job/JobOutput/useJobEvents.js b/awx/ui/src/screens/Job/JobOutput/useJobEvents.js index 5e63ee0497..5f2002912b 100644 --- a/awx/ui/src/screens/Job/JobOutput/useJobEvents.js +++ b/awx/ui/src/screens/Job/JobOutput/useJobEvents.js @@ -11,16 +11,21 @@ const initialState = { // events with parent events that aren't yet loaded. // arrays indexed by parent uuid eventsWithoutParents: {}, + // object in the form { counter: {rowNumber: n, numChildren: m}} for parent nodes + childrenSummary: {}, + // parent_uuid's for "meta" events that need to be injected into the tree to + // maintain tree integrity + metaEventParentUuid: {}, isAllCollapsed: false, }; export const ADD_EVENTS = 'ADD_EVENTS'; export const TOGGLE_NODE_COLLAPSED = 'TOGGLE_NODE_COLLAPSED'; -export const SET_EVENT_NUM_CHILDREN = 'SET_EVENT_NUM_CHILDREN'; export const CLEAR_EVENTS = 'CLEAR_EVENTS'; export const REBUILD_TREE = 'REBUILD_TREE'; export const TOGGLE_COLLAPSE_ALL = 'TOGGLE_COLLAPSE_ALL'; +export const SET_CHILDREN_SUMMARY = 'SET_CHILDREN_SUMMARY'; -export default function useJobEvents(callbacks, isFlatMode) { +export default function useJobEvents(callbacks, jobId, isFlatMode) { const [actionQueue, setActionQueue] = useState([]); const enqueueAction = (action) => { setActionQueue((queue) => queue.concat(action)); @@ -42,6 +47,31 @@ export default function useJobEvents(callbacks, isFlatMode) { }); }, [actionQueue]); + useEffect(() => { + if (isFlatMode) { + return; + } + + callbacks + .fetchChildrenSummary() + .then((result) => { + if (result.data.event_processing_finished === false) { + callbacks.setForceFlatMode(true); + callbacks.setJobTreeReady(); + return; + } + enqueueAction({ + type: SET_CHILDREN_SUMMARY, + childrenSummary: result.data.children_summary, + metaEventParentUuid: result.data.meta_event_nested_uuid, + }); + }) + .catch(() => { + callbacks.setForceFlatMode(true); + callbacks.setJobTreeReady(); + }); + }, [jobId, isFlatMode]); // eslint-disable-line react-hooks/exhaustive-deps + return { addEvents: (events) => dispatch({ type: ADD_EVENTS, events }), getNodeByUuid: (uuid) => getNodeByUuid(state, uuid), @@ -53,10 +83,14 @@ export default function useJobEvents(callbacks, isFlatMode) { getNodeForRow: (rowIndex) => getNodeForRow(state, rowIndex), getTotalNumChildren: (uuid) => { const node = getNodeByUuid(state, uuid); - return getTotalNumChildren(node); + return getTotalNumChildren(node, state.childrenSummary); }, getNumCollapsedEvents: () => - state.tree.reduce((sum, node) => sum + getNumCollapsedChildren(node), 0), + state.tree.reduce( + (sum, node) => + sum + getNumCollapsedChildren(node, state.childrenSummary), + 0 + ), getCounterForRow: (rowIndex) => getCounterForRow(state, rowIndex), getEvent: (eventIndex) => getEvent(state, eventIndex), clearLoadedEvents: () => dispatch({ type: CLEAR_EVENTS }), @@ -74,12 +108,17 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { return toggleCollapseAll(state, action.isCollapsed); case TOGGLE_NODE_COLLAPSED: return toggleNodeIsCollapsed(state, action.uuid); - case SET_EVENT_NUM_CHILDREN: - return setEventNumChildren(state, action.uuid, action.numChildren); case CLEAR_EVENTS: return initialState; case REBUILD_TREE: return rebuildTree(state); + case SET_CHILDREN_SUMMARY: + callbacks.setJobTreeReady(); + return { + ...state, + childrenSummary: action.childrenSummary || {}, + metaEventParentUuid: action.metaEventParentUuid || {}, + }; default: throw new Error(`Unrecognized action: ${action.type}`); } @@ -100,6 +139,9 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { throw new Error('Cannot add event; missing rowNumber'); } const eventIndex = event.counter; + if (!event.parent_uuid && state.metaEventParentUuid[eventIndex]) { + event.parent_uuid = state.metaEventParentUuid[eventIndex]; + } if (state.events[eventIndex]) { state.events[eventIndex] = event; state = _gatherEventsForNewParent(state, event.uuid); @@ -113,22 +155,21 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { let isParentFound; [state, isParentFound] = _addNestedLevelEvent(state, event); if (!isParentFound) { - parentsToFetch[event.parent_uuid] = { - childCounter: event.counter, - childRowNumber: event.rowNumber, - }; + parentsToFetch[event.parent_uuid] = true; state = _addEventWithoutParent(state, event); } }); Object.keys(parentsToFetch).forEach(async (uuid) => { - const { childCounter, childRowNumber } = parentsToFetch[uuid]; const parent = await callbacks.fetchEventByUuid(uuid); - const numPrevSiblings = await callbacks.fetchNumEvents( - parent.counter, - childCounter - ); - parent.rowNumber = childRowNumber - numPrevSiblings - 1; + + if (!state.childrenSummary || !state.childrenSummary[parent.counter]) { + // eslint-disable-next-line no-console + console.error('No row number found for ', parent.counter); + return; + } + parent.rowNumber = state.childrenSummary[parent.counter].rowNumber; + enqueueAction({ type: ADD_EVENTS, events: [parent], @@ -180,7 +221,6 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { const index = parent.children.findIndex( (node) => node.eventIndex >= eventIndex ); - const length = parent.children.length + 1; if (index === -1) { state = updateNodeByUuid(state, event.parent_uuid, (node) => { node.children.push(newNode); @@ -206,9 +246,6 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { }, event.uuid ); - if (length === 1) { - _fetchNumChildren(state, parent); - } return [state, true]; } @@ -231,45 +268,6 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { }; } - async function _fetchNumChildren(state, node) { - const event = state.events[node.eventIndex]; - if (!event) { - throw new Error( - `Cannot fetch numChildren; event ${node.eventIndex} not found` - ); - } - const sibling = await _getNextSibling(state, event); - const numChildren = await callbacks.fetchNumEvents( - event.counter, - sibling?.counter - ); - enqueueAction({ - type: SET_EVENT_NUM_CHILDREN, - uuid: event.uuid, - numChildren, - }); - if (sibling) { - sibling.rowNumber = event.rowNumber + numChildren + 1; - enqueueAction({ - type: ADD_EVENTS, - events: [sibling], - }); - } - } - - async function _getNextSibling(state, event) { - if (!event.parent_uuid) { - return callbacks.fetchNextRootNode(event.counter); - } - const parentNode = getNodeByUuid(state, event.parent_uuid); - const parent = state.events[parentNode.eventIndex]; - const sibling = await callbacks.fetchNextSibling(parent.id, event.counter); - if (!sibling) { - return _getNextSibling(state, parent); - } - return sibling; - } - function _gatherEventsForNewParent(state, parentUuid) { if (!state.eventsWithoutParents[parentUuid]) { return state; @@ -303,8 +301,13 @@ function getEventForRow(state, rowIndex) { return null; } -function getNodeForRow(state, rowToFind) { - const { node } = _getNodeForRow(state, rowToFind, state.tree); +function getNodeForRow(state, rowToFind, childrenSummary) { + const { node } = _getNodeForRow( + state, + rowToFind, + state.tree, + childrenSummary + ); return node; } @@ -329,8 +332,14 @@ function _getNodeForRow(state, rowToFind, nodes) { if (event.rowNumber === rowToFind) { return { node }; } - const totalNodeDescendants = getTotalNumChildren(node); - const numCollapsedChildren = getNumCollapsedChildren(node); + const totalNodeDescendants = getTotalNumChildren( + node, + state.childrenSummary + ); + const numCollapsedChildren = getNumCollapsedChildren( + node, + state.childrenSummary + ); const nodeChildren = totalNodeDescendants - numCollapsedChildren; if (event.rowNumber + nodeChildren >= rowToFind) { // requested row is in children/descendants @@ -370,8 +379,8 @@ function _getNodeForRow(state, rowToFind, nodes) { function _getNodeInChildren(state, node, rowToFind) { const event = state.events[node.eventIndex]; - const firstChild = state.events[node.children[0].eventIndex]; - if (rowToFind < firstChild.rowNumber) { + const firstChild = state.events[node.children[0]?.eventIndex]; + if (!firstChild || rowToFind < firstChild.rowNumber) { const rowDiff = rowToFind - event.rowNumber; return { node: null, @@ -391,25 +400,25 @@ function _getLastDescendantNode(nodes) { return lastDescendant; } -function getTotalNumChildren(node) { - if (typeof node.numChildren !== 'undefined') { - return node.numChildren; +function getTotalNumChildren(node, childrenSummary) { + if (childrenSummary[node.eventIndex]) { + return childrenSummary[node.eventIndex].numChildren; } let estimatedNumChildren = node.children.length; node.children.forEach((child) => { - estimatedNumChildren += getTotalNumChildren(child); + estimatedNumChildren += getTotalNumChildren(child, childrenSummary); }); return estimatedNumChildren; } -function getNumCollapsedChildren(node) { +function getNumCollapsedChildren(node, childrenSummary) { if (node.isCollapsed) { - return getTotalNumChildren(node); + return getTotalNumChildren(node, childrenSummary); } let sum = 0; node.children.forEach((child) => { - sum += getNumCollapsedChildren(child); + sum += getNumCollapsedChildren(child, childrenSummary); }); return sum; } @@ -514,16 +523,6 @@ function _getNodeByIndex(arr, index) { return _getNodeByIndex(arr[i - 1].children, index); } -function setEventNumChildren(state, uuid, numChildren) { - if (!state.uuidMap[uuid]) { - return state; - } - return updateNodeByUuid(state, uuid, (node) => ({ - ...node, - numChildren, - })); -} - function getEvent(state, eventIndex) { const event = state.events[eventIndex]; if (event) { diff --git a/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js b/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js index e0283e5acc..1e398b7b3d 100644 --- a/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js +++ b/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js @@ -8,24 +8,22 @@ import useJobEvents, { SET_EVENT_NUM_CHILDREN, } from './useJobEvents'; -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - function Child() { return
; } function HookTest({ fetchEventByUuid = () => {}, - fetchNextSibling = () => {}, - fetchNextRootNode = () => {}, - fetchNumEvents = () => {}, + fetchChildrenSummary = () => {}, + setForceFlatMode = () => {}, + setJobTreeReady = () => {}, isFlatMode = false, }) { const hookFuncs = useJobEvents( { fetchEventByUuid, - fetchNextSibling, - fetchNextRootNode, - fetchNumEvents, + fetchChildrenSummary, + setForceFlatMode, + setJobTreeReady, }, isFlatMode ); @@ -153,19 +151,19 @@ describe('useJobEvents', () => { beforeEach(() => { callbacks = { fetchEventByUuid: jest.fn(), - fetchNextSibling: jest.fn(), - fetchNextRootNode: jest.fn(), - fetchNumEvents: jest.fn(), + fetchChildrenSummary: jest.fn(), + setForceFlatMode: jest.fn(), + setJobTreeReady: jest.fn(), }; enqueueAction = jest.fn(); - callbacks.fetchNextSibling.mockResolvedValue(eventsList[9]); - callbacks.fetchNextRootNode.mockResolvedValue(eventsList[9]); reducer = jobEventsReducer(callbacks, false, enqueueAction); emptyState = { tree: [], events: {}, uuidMap: {}, eventsWithoutParents: {}, + childrenSummary: {}, + metaEventParentUuid: {}, eventGaps: [], isAllCollapsed: false, }; @@ -380,10 +378,18 @@ describe('useJobEvents', () => { callbacks.fetchEventByUuid.mockResolvedValue({ counter: 10, }); - const state = reducer(emptyState, { - type: ADD_EVENTS, - events: eventsList, - }); + const state = reducer( + { + ...emptyState, + childrenSummary: { + 10: [9, 2], + }, + }, + { + type: ADD_EVENTS, + events: eventsList, + } + ); const newEvents = [ { @@ -404,10 +410,18 @@ describe('useJobEvents', () => { callbacks.fetchEventByUuid.mockResolvedValue({ counter: 10, }); - const state = reducer(emptyState, { - type: ADD_EVENTS, - events: eventsList, - }); + const state = reducer( + { + ...emptyState, + childrenSummary: { + 10: [9, 2], + }, + }, + { + type: ADD_EVENTS, + events: eventsList, + } + ); const newEvents = [ { @@ -437,10 +451,18 @@ describe('useJobEvents', () => { callbacks.fetchEventByUuid.mockResolvedValue({ counter: 10, }); - const state = reducer(emptyState, { - type: ADD_EVENTS, - events: eventsList, - }); + const state = reducer( + { + ...emptyState, + childrenSummary: { + 10: [9, 1], + }, + }, + { + type: ADD_EVENTS, + events: eventsList, + } + ); const newEvents = [ { @@ -471,10 +493,18 @@ describe('useJobEvents', () => { callbacks.fetchEventByUuid.mockResolvedValue({ counter: 10, }); - const state = reducer(emptyState, { - type: ADD_EVENTS, - events: eventsList, - }); + const state = reducer( + { + ...emptyState, + childrenSummary: { + 10: [9, 2], + }, + }, + { + type: ADD_EVENTS, + events: eventsList, + } + ); const newEvents = [ { @@ -561,10 +591,19 @@ describe('useJobEvents', () => { event_level: 2, parent_uuid: 'abc-002', }; - const state = reducer(emptyState, { - type: ADD_EVENTS, - events: [event3], - }); + const state = reducer( + { + ...emptyState, + childrenSummary: { + 1: [0, 3], + 2: [1, 2], + }, + }, + { + type: ADD_EVENTS, + events: [event3], + } + ); expect(callbacks.fetchEventByUuid).toHaveBeenCalledWith('abc-002'); const event2 = { @@ -741,152 +780,49 @@ describe('useJobEvents', () => { }); }); - describe('fetchNumChildren', () => { - test('should find child count for root node', async () => { - callbacks.fetchNextRootNode.mockResolvedValue({ - id: 121, - counter: 21, - rowNumber: 20, - uuid: 'abc-021', - event_level: 0, - parent_uuid: '', - }); - callbacks.fetchNumEvents.mockResolvedValue(19); - reducer(emptyState, { - type: ADD_EVENTS, - events: [eventsList[0], eventsList[1]], - }); - - expect(callbacks.fetchNextSibling).toHaveBeenCalledTimes(0); - expect(callbacks.fetchNextRootNode).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNextRootNode).toHaveBeenCalledWith(1); - await sleep(0); - expect(callbacks.fetchNumEvents).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNumEvents).toHaveBeenCalledWith(1, 21); - expect(enqueueAction).toHaveBeenCalledWith({ - type: SET_EVENT_NUM_CHILDREN, - uuid: 'abc-001', - numChildren: 19, - }); - }); - - test('should find child count for last root node', async () => { - callbacks.fetchNextRootNode.mockResolvedValue(null); - callbacks.fetchNumEvents.mockResolvedValue(19); - reducer(emptyState, { - type: ADD_EVENTS, - events: [eventsList[0], eventsList[1]], - }); - - expect(callbacks.fetchNextSibling).toHaveBeenCalledTimes(0); - expect(callbacks.fetchNextRootNode).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNextRootNode).toHaveBeenCalledWith(1); - await sleep(0); - expect(callbacks.fetchNumEvents).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNumEvents).toHaveBeenCalledWith(1, undefined); - expect(enqueueAction).toHaveBeenCalledWith({ - type: SET_EVENT_NUM_CHILDREN, - uuid: 'abc-001', - numChildren: 19, - }); - }); - - test('should find child count for nested node', async () => { - const state = { - events: { - 1: eventsList[0], - 2: eventsList[1], + test('should nest "meta" event based on given parent uuid', () => { + const state = reducer( + { + ...emptyState, + childrenSummary: { + 2: { rowNumber: 1, numChildren: 3 }, }, - tree: [ + metaEventParentUuid: { + 4: 'abc-002', + }, + }, + { + type: ADD_EVENTS, + events: [...eventsList.slice(0, 3)], + } + ); + const state2 = reducer(state, { + type: ADD_EVENTS, + events: [ + { + counter: 4, + rowNumber: 3, + parent_uuid: '', + }, + ], + }); + + expect(state2.tree).toEqual([ + { + eventIndex: 1, + isCollapsed: false, + children: [ { - children: [{ children: [], eventIndex: 2, isCollapsed: false }], - eventIndex: 1, + eventIndex: 2, isCollapsed: false, + children: [ + { eventIndex: 3, isCollapsed: false, children: [] }, + { eventIndex: 4, isCollapsed: false, children: [] }, + ], }, ], - uuidMap: { - 'abc-001': 1, - 'abc-002': 2, - }, - eventsWithoutParents: {}, - }; - - callbacks.fetchNextSibling.mockResolvedValue({ - id: 20, - counter: 20, - rowNumber: 19, - uuid: 'abc-020', - event_level: 1, - parent_uuid: 'abc-001', - }); - callbacks.fetchNumEvents.mockResolvedValue(18); - reducer(state, { - type: ADD_EVENTS, - events: [eventsList[2]], - }); - - expect(callbacks.fetchNextSibling).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNextSibling).toHaveBeenCalledWith(101, 2); - await sleep(0); - expect(callbacks.fetchNextRootNode).toHaveBeenCalledTimes(0); - expect(callbacks.fetchNumEvents).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNumEvents).toHaveBeenCalledWith(2, 20); - expect(enqueueAction).toHaveBeenCalledWith({ - type: SET_EVENT_NUM_CHILDREN, - uuid: 'abc-002', - numChildren: 18, - }); - }); - - test('should find child count for nested node, last sibling', async () => { - const state = { - events: { - 1: eventsList[0], - 2: eventsList[1], - }, - tree: [ - { - children: [{ children: [], eventIndex: 2, isCollapsed: false }], - eventIndex: 1, - isCollapsed: false, - }, - ], - uuidMap: { - 'abc-001': 1, - 'abc-002': 2, - }, - eventsWithoutParents: {}, - }; - - callbacks.fetchNextSibling.mockResolvedValue(null); - callbacks.fetchNextRootNode.mockResolvedValue({ - id: 121, - counter: 21, - rowNumber: 20, - uuid: 'abc-021', - event_level: 0, - parent_uuid: '', - }); - callbacks.fetchNumEvents.mockResolvedValue(19); - reducer(state, { - type: ADD_EVENTS, - events: [eventsList[2]], - }); - - expect(callbacks.fetchNextSibling).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNextSibling).toHaveBeenCalledWith(101, 2); - await sleep(0); - expect(callbacks.fetchNextRootNode).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNextRootNode).toHaveBeenCalledWith(1); - await sleep(0); - expect(callbacks.fetchNumEvents).toHaveBeenCalledTimes(1); - expect(callbacks.fetchNumEvents).toHaveBeenCalledWith(2, 21); - expect(enqueueAction).toHaveBeenCalledWith({ - type: SET_EVENT_NUM_CHILDREN, - uuid: 'abc-002', - numChildren: 19, - }); - }); + }, + ]); }); }); @@ -968,40 +904,6 @@ describe('useJobEvents', () => { }); }); - describe('setEventNumChildren', () => { - test('should set number of children on root node', () => { - const state = reducer(emptyState, { - type: ADD_EVENTS, - events: eventsList, - }); - expect(state.tree[0].numChildren).toEqual(undefined); - - const { tree } = reducer(state, { - type: SET_EVENT_NUM_CHILDREN, - uuid: 'abc-001', - numChildren: 8, - }); - - expect(tree[0].numChildren).toEqual(8); - }); - - test('should set number of children on nested node', () => { - const state = reducer(emptyState, { - type: ADD_EVENTS, - events: eventsList, - }); - expect(state.tree[0].numChildren).toEqual(undefined); - - const { tree } = reducer(state, { - type: SET_EVENT_NUM_CHILDREN, - uuid: 'abc-006', - numChildren: 3, - }); - - expect(tree[0].children[1].numChildren).toEqual(3); - }); - }); - describe('getNodeForRow', () => { let wrapper; beforeEach(() => { @@ -1266,16 +1168,19 @@ describe('useJobEvents', () => { }); test('should get node after gap in loaded children', async () => { - const fetchNumEvents = jest.fn(); - fetchNumEvents.mockImplementation((index) => { - const counts = { - 1: 52, - 2: 3, - 6: 47, - }; - return Promise.resolve(counts[index]); + const fetchChildrenSummary = jest.fn(); + fetchChildrenSummary.mockResolvedValue({ + data: { + children_summary: { + 1: { rowNumber: 0, numChildren: 52 }, + 2: { rowNumber: 1, numChildren: 3 }, + 6: { rowNumber: 5, numChildren: 47 }, + }, + meta_event_nested_uuid: {}, + }, }); - wrapper = mount(); + + wrapper = mount(); const laterEvents = [ { id: 151, @@ -1424,13 +1329,12 @@ describe('useJobEvents', () => { }); test('should return estimated counter when node is non-loaded child', async () => { - callbacks.fetchNumEvents.mockImplementation((counter) => { - const children = { - 1: 28, - 2: 3, - 6: 23, - }; - return children[counter]; + callbacks.fetchChildrenSummary.mockResolvedValue({ + data: { + 1: { rowNumber: 0, numChildren: 28 }, + 2: { rowNumber: 1, numChildren: 3 }, + 6: { rowNumber: 5, numChidren: 23 }, + }, }); const wrapper = mount(); wrapper.update(); @@ -1463,13 +1367,15 @@ describe('useJobEvents', () => { }); test('should estimate counter after skipping collapsed subtree', async () => { - callbacks.fetchNumEvents.mockImplementation((counter) => { - const children = { - 1: 85, - 2: 66, - 69: 17, - }; - return children[counter]; + callbacks.fetchChildrenSummary.mockResolvedValue({ + data: { + children_summary: { + 1: { rowNumber: 0, numChildren: 85 }, + 2: { rowNumber: 1, numChildren: 66 }, + 69: { rowNumber: 68, numChildren: 17 }, + }, + meta_event_nested_uuid: {}, + }, }); const wrapper = mount(); await act(async () => { @@ -1497,12 +1403,14 @@ describe('useJobEvents', () => { }); test('should estimate counter in gap between loaded events', async () => { - callbacks.fetchNumEvents.mockImplementation( - (counter) => - ({ - 1: 30, - }[counter]) - ); + callbacks.fetchChildrenSummary.mockResolvedValue({ + data: { + children_summary: { + 1: { rowNumber: 0, numChildren: 30 }, + }, + meta_event_nested_uuid: {}, + }, + }); const wrapper = mount(); await act(async () => { wrapper.find('#test').prop('addEvents')([ @@ -1556,12 +1464,14 @@ describe('useJobEvents', () => { }); test('should estimate counter in gap before loaded sibling events', async () => { - callbacks.fetchNumEvents.mockImplementation( - (counter) => - ({ - 1: 30, - }[counter]) - ); + callbacks.fetchChildrenSummary.mockResolvedValue({ + data: { + children_summary: { + 1: { rowNumber: 0, numChildren: 30 }, + }, + meta_event_nested_uuid: {}, + }, + }); const wrapper = mount(); await act(async () => { wrapper.find('#test').prop('addEvents')([ @@ -1599,12 +1509,14 @@ describe('useJobEvents', () => { }); test('should get counter for node between unloaded siblings', async () => { - callbacks.fetchNumEvents.mockImplementation( - (counter) => - ({ - 1: 30, - }[counter]) - ); + callbacks.fetchChildrenSummary.mockResolvedValue({ + data: { + children_summary: { + 1: { rowNumber: 0, numChildren: 30 }, + }, + meta_event_nested_uuid: {}, + }, + }); const wrapper = mount(); await act(async () => { wrapper.find('#test').prop('addEvents')([