Use new children-summary endpoint data to traverse job event tree (#11944)

* use new children-summary endpoint data to traverse job event tree

* update job output tests for new children summary data

* force flat mode if event child summary fails to load

* update childrenSummary data for endpoint changes

* don't add jobs to job tree until children summary loaded

* force job output into flat mode if job processing not complete
This commit is contained in:
Keith Grant 2022-04-06 10:10:04 -07:00 committed by GitHub
parent bea924ddc6
commit 7cbb783b2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 275 additions and 405 deletions

View File

@ -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;

View File

@ -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}
/>

View File

@ -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],
},
});
};

View File

@ -4,7 +4,6 @@ export default styled.div`
display: flex;
&:hover {
background-color: white;
cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')};
}

View File

@ -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) {

View File

@ -8,24 +8,22 @@ import useJobEvents, {
SET_EVENT_NUM_CHILDREN,
} from './useJobEvents';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function Child() {
return <div />;
}
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(<HookTest fetchNumEvents={fetchNumEvents} />);
wrapper = mount(<HookTest fetchChildrenSummary={fetchChildrenSummary} />);
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(<HookTest {...callbacks} />);
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(<HookTest {...callbacks} />);
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(<HookTest {...callbacks} />);
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(<HookTest {...callbacks} />);
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(<HookTest {...callbacks} />);
await act(async () => {
wrapper.find('#test').prop('addEvents')([