diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index 1a26cd9409..c6f68cf739 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -168,12 +168,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { const { addEvents, toggleNodeIsCollapsed, + toggleCollapseAll, getEventForRow, getNumCollapsedEvents, getCounterForRow, getEvent, clearLoadedEvents, rebuildEventsTree, + isAllCollapsed, } = useJobEvents( { fetchEventByUuid, @@ -504,7 +506,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { isCollapsed={node.isCollapsed} hasChildren={node.children.length} onToggleCollapsed={() => { - toggleNodeIsCollapsed(event.uuid); + toggleNodeIsCollapsed(event.uuid, !node.isCollapsed); }} /> ) : ( @@ -653,6 +655,10 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { scrollHeight.current = e.scrollHeight; }; + const handleExpandCollapseAll = () => { + toggleCollapseAll(!isAllCollapsed); + }; + if (contentError) { return ; } @@ -696,6 +702,10 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { onScrollLast={handleScrollLast} onScrollNext={handleScrollNext} onScrollPrevious={handleScrollPrevious} + toggleExpandCollapseAll={handleExpandCollapseAll} + isFlatMode={isFlatMode} + isTemplateJob={job.type === 'job'} + isAllCollapsed={isAllCollapsed} /> Button { + padding-left: 8px; + } +`; const PageControls = ({ onScrollFirst, onScrollLast, onScrollNext, onScrollPrevious, + toggleExpandCollapseAll, + isAllCollapsed, + isFlatMode, + isTemplateJob, }) => ( - - - - - - + <> + + + {!isFlatMode && isTemplateJob && ( + + )} + + + + + + + + + ); export default PageControls; diff --git a/awx/ui/src/screens/Job/JobOutput/PageControls.test.js b/awx/ui/src/screens/Job/JobOutput/PageControls.test.js index 94fa3e74ac..65c5a8b684 100644 --- a/awx/ui/src/screens/Job/JobOutput/PageControls.test.js +++ b/awx/ui/src/screens/Job/JobOutput/PageControls.test.js @@ -22,11 +22,42 @@ describe('PageControls', () => { }); test('should render menu control icons', () => { - wrapper = mountWithContexts(); + wrapper = mountWithContexts(); findChildren(); expect(AngleDoubleUpIcon.length).toBe(1); expect(AngleDoubleDownIcon.length).toBe(1); expect(AngleUpIcon.length).toBe(1); expect(AngleDownIcon.length).toBe(1); }); + + test('should render expand/collapse all', () => { + wrapper = mountWithContexts( + + ); + const expandCollapse = wrapper.find('PageControls__ExpandCollapseWrapper'); + expect(expandCollapse).toHaveLength(1); + expect(expandCollapse.find('AngleDownIcon')).toHaveLength(1); + expect(expandCollapse.find('AngleRightIcon')).toHaveLength(0); + }); + + test('should render correct expand/collapse angle icon', () => { + wrapper = mountWithContexts( + + ); + + const expandCollapse = wrapper.find('PageControls__ExpandCollapseWrapper'); + expect(expandCollapse).toHaveLength(1); + expect(expandCollapse.find('AngleDownIcon')).toHaveLength(0); + expect(expandCollapse.find('AngleRightIcon')).toHaveLength(1); + }); + + test('Should not render expand/collapse all', () => { + wrapper = mountWithContexts( + + ); + + const expandCollapse = wrapper.find('PageControls__ExpandCollapseWrapper'); + expect(expandCollapse.find('AngleDownIcon')).toHaveLength(0); + expect(expandCollapse.find('AngleRightIcon')).toHaveLength(0); + }); }); diff --git a/awx/ui/src/screens/Job/JobOutput/useJobEvents.js b/awx/ui/src/screens/Job/JobOutput/useJobEvents.js index 02cf0fa0bd..d2c848b7e3 100644 --- a/awx/ui/src/screens/Job/JobOutput/useJobEvents.js +++ b/awx/ui/src/screens/Job/JobOutput/useJobEvents.js @@ -10,12 +10,14 @@ const initialState = { // events with parent events that aren't yet loaded. // arrays indexed by parent uuid eventsWithoutParents: {}, + 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 default function useJobEvents(callbacks, isFlatMode) { const [actionQueue, setActionQueue] = useState([]); @@ -24,7 +26,6 @@ export default function useJobEvents(callbacks, isFlatMode) { }; const reducer = jobEventsReducer(callbacks, isFlatMode, enqueueAction); const [state, dispatch] = useReducer(reducer, initialState); - useEffect(() => { setActionQueue((queue) => { const action = queue[0]; @@ -43,8 +44,10 @@ export default function useJobEvents(callbacks, isFlatMode) { return { addEvents: (events) => dispatch({ type: ADD_EVENTS, events }), getNodeByUuid: (uuid) => getNodeByUuid(state, uuid), - toggleNodeIsCollapsed: (uuid) => - dispatch({ type: TOGGLE_NODE_COLLAPSED, uuid }), + toggleNodeIsCollapsed: (uuid, isCollapsed) => + dispatch({ type: TOGGLE_NODE_COLLAPSED, uuid, isCollapsed }), + toggleCollapseAll: (isCollapsed) => + dispatch({ type: TOGGLE_COLLAPSE_ALL, isCollapsed }), getEventForRow: (rowIndex) => getEventForRow(state, rowIndex), getNodeForRow: (rowIndex) => getNodeForRow(state, rowIndex), getTotalNumChildren: (uuid) => { @@ -57,6 +60,7 @@ export default function useJobEvents(callbacks, isFlatMode) { getEvent: (eventIndex) => getEvent(state, eventIndex), clearLoadedEvents: () => dispatch({ type: CLEAR_EVENTS }), rebuildEventsTree: () => dispatch({ type: REBUILD_TREE }), + isAllCollapsed: state.isAllCollapsed, }; } @@ -65,6 +69,8 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { switch (action.type) { case ADD_EVENTS: return addEvents(state, action.events); + case TOGGLE_COLLAPSE_ALL: + return toggleCollapseAll(state, action.isCollapsed); case TOGGLE_NODE_COLLAPSED: return toggleNodeIsCollapsed(state, action.uuid); case SET_EVENT_NUM_CHILDREN: @@ -135,7 +141,7 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { const eventIndex = event.counter; const newNode = { eventIndex, - isCollapsed: false, + isCollapsed: state.isAllCollapsed, children: [], }; const index = state.tree.findIndex((node) => node.eventIndex > eventIndex); @@ -167,7 +173,7 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) { } const newNode = { eventIndex, - isCollapsed: false, + isCollapsed: state.isAllCollapsed, children: [], }; const index = parent.children.findIndex( @@ -400,7 +406,6 @@ function getNumCollapsedChildren(node) { if (node.isCollapsed) { return getTotalNumChildren(node); } - let sum = 0; node.children.forEach((child) => { sum += getNumCollapsedChildren(child); @@ -409,10 +414,40 @@ function getNumCollapsedChildren(node) { } function toggleNodeIsCollapsed(state, eventUuid) { - return updateNodeByUuid(state, eventUuid, (node) => ({ + return { + ...updateNodeByUuid(state, eventUuid, (node) => ({ + ...node, + isCollapsed: !node.isCollapsed, + })), + isAllCollapsed: false, + }; +} + +function toggleCollapseAll(state, isAllCollapsed) { + const newTree = state.tree.map((node) => + _toggleNestedNodes(state.events, node, isAllCollapsed) + ); + return { ...state, tree: newTree, isAllCollapsed }; +} + +function _toggleNestedNodes(events, node, isCollapsed) { + const { + parent_uuid, + event_data: { playbook_uuid }, + uuid, + } = events[node.eventIndex]; + + const eventShouldNotCollapse = uuid === playbook_uuid || !parent_uuid?.length; + + const children = node.children?.map((nestedNode) => + _toggleNestedNodes(events, nestedNode, isCollapsed) + ); + + return { ...node, - isCollapsed: !node.isCollapsed, - })); + isCollapsed: eventShouldNotCollapse ? false : isCollapsed, + children, + }; } function updateNodeByUuid(state, uuid, update) { diff --git a/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js b/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js index 1f19db3750..e0283e5acc 100644 --- a/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js +++ b/awx/ui/src/screens/Job/JobOutput/useJobEvents.test.js @@ -167,6 +167,7 @@ describe('useJobEvents', () => { uuidMap: {}, eventsWithoutParents: {}, eventGaps: [], + isAllCollapsed: false, }; });