Merge pull request #11517 from AlexSCorey/11236-ExpandCollapseAll

Adds expand collapse all functionality on job output page.
This commit is contained in:
Sarah Akus
2022-02-02 09:43:13 -05:00
committed by GitHub
5 changed files with 159 additions and 47 deletions

View File

@@ -168,12 +168,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
const { const {
addEvents, addEvents,
toggleNodeIsCollapsed, toggleNodeIsCollapsed,
toggleCollapseAll,
getEventForRow, getEventForRow,
getNumCollapsedEvents, getNumCollapsedEvents,
getCounterForRow, getCounterForRow,
getEvent, getEvent,
clearLoadedEvents, clearLoadedEvents,
rebuildEventsTree, rebuildEventsTree,
isAllCollapsed,
} = useJobEvents( } = useJobEvents(
{ {
fetchEventByUuid, fetchEventByUuid,
@@ -504,7 +506,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
isCollapsed={node.isCollapsed} isCollapsed={node.isCollapsed}
hasChildren={node.children.length} hasChildren={node.children.length}
onToggleCollapsed={() => { onToggleCollapsed={() => {
toggleNodeIsCollapsed(event.uuid); toggleNodeIsCollapsed(event.uuid, !node.isCollapsed);
}} }}
/> />
) : ( ) : (
@@ -653,6 +655,10 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
scrollHeight.current = e.scrollHeight; scrollHeight.current = e.scrollHeight;
}; };
const handleExpandCollapseAll = () => {
toggleCollapseAll(!isAllCollapsed);
};
if (contentError) { if (contentError) {
return <ContentError error={contentError} />; return <ContentError error={contentError} />;
} }
@@ -696,6 +702,10 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
onScrollLast={handleScrollLast} onScrollLast={handleScrollLast}
onScrollNext={handleScrollNext} onScrollNext={handleScrollNext}
onScrollPrevious={handleScrollPrevious} onScrollPrevious={handleScrollPrevious}
toggleExpandCollapseAll={handleExpandCollapseAll}
isFlatMode={isFlatMode}
isTemplateJob={job.type === 'job'}
isAllCollapsed={isAllCollapsed}
/> />
<OutputWrapper cssMap={cssMap}> <OutputWrapper cssMap={cssMap}>
<InfiniteLoader <InfiniteLoader

View File

@@ -1,6 +1,6 @@
import 'styled-components/macro';
import React from 'react'; import React from 'react';
import 'styled-components/macro';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core'; import { Button } from '@patternfly/react-core';
import { import {
@@ -8,57 +8,92 @@ import {
AngleDoubleDownIcon, AngleDoubleDownIcon,
AngleUpIcon, AngleUpIcon,
AngleDownIcon, AngleDownIcon,
AngleRightIcon,
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
const Wrapper = styled.div` const ControllsWrapper = styled.div`
display: flex; display: flex;
height: 35px; height: 35px;
border: 1px solid #d7d7d7; border: 1px solid #d7d7d7;
width: 100%; width: 100%;
justify-content: space-between;
`;
const ScrollWrapper = styled.div`
display: flex;
justify-content: flex-end; justify-content: flex-end;
`; `;
const ExpandCollapseWrapper = styled.div`
display: flex;
justify-content: flex-start;
& > Button {
padding-left: 8px;
}
`;
const PageControls = ({ const PageControls = ({
onScrollFirst, onScrollFirst,
onScrollLast, onScrollLast,
onScrollNext, onScrollNext,
onScrollPrevious, onScrollPrevious,
toggleExpandCollapseAll,
isAllCollapsed,
isFlatMode,
isTemplateJob,
}) => ( }) => (
<Wrapper> <>
<Button <ControllsWrapper>
ouiaId="job-output-scroll-previous-button" <ExpandCollapseWrapper>
aria-label={t`Scroll previous`} {!isFlatMode && isTemplateJob && (
onClick={onScrollPrevious} <Button
variant="plain" aria-label={
> isAllCollapsed ? t`Expand job events` : t`Collapse all job events`
<AngleUpIcon /> }
</Button> variant="plain"
<Button type="button"
ouiaId="job-output-scroll-next-button" onClick={toggleExpandCollapseAll}
aria-label={t`Scroll next`} >
onClick={onScrollNext} {isAllCollapsed ? <AngleRightIcon /> : <AngleDownIcon />}
variant="plain" </Button>
> )}
<AngleDownIcon /> </ExpandCollapseWrapper>
</Button> <ScrollWrapper>
<Button <Button
ouiaId="job-output-scroll-first-button" ouiaId="job-output-scroll-previous-button"
aria-label={t`Scroll first`} aria-label={t`Scroll previous`}
onClick={onScrollFirst} onClick={onScrollPrevious}
variant="plain" variant="plain"
> >
<AngleDoubleUpIcon /> <AngleUpIcon />
</Button> </Button>
<Button <Button
ouiaId="job-output-scroll-last-button" ouiaId="job-output-scroll-next-button"
aria-label={t`Scroll last`} aria-label={t`Scroll next`}
onClick={onScrollLast} onClick={onScrollNext}
variant="plain" variant="plain"
> >
<AngleDoubleDownIcon /> <AngleDownIcon />
</Button> </Button>
</Wrapper> <Button
ouiaId="job-output-scroll-first-button"
aria-label={t`Scroll first`}
onClick={onScrollFirst}
variant="plain"
>
<AngleDoubleUpIcon />
</Button>
<Button
ouiaId="job-output-scroll-last-button"
aria-label={t`Scroll last`}
onClick={onScrollLast}
variant="plain"
>
<AngleDoubleDownIcon />
</Button>
</ScrollWrapper>
</ControllsWrapper>
</>
); );
export default PageControls; export default PageControls;

View File

@@ -22,11 +22,42 @@ describe('PageControls', () => {
}); });
test('should render menu control icons', () => { test('should render menu control icons', () => {
wrapper = mountWithContexts(<PageControls />); wrapper = mountWithContexts(<PageControls isFlatMode />);
findChildren(); findChildren();
expect(AngleDoubleUpIcon.length).toBe(1); expect(AngleDoubleUpIcon.length).toBe(1);
expect(AngleDoubleDownIcon.length).toBe(1); expect(AngleDoubleDownIcon.length).toBe(1);
expect(AngleUpIcon.length).toBe(1); expect(AngleUpIcon.length).toBe(1);
expect(AngleDownIcon.length).toBe(1); expect(AngleDownIcon.length).toBe(1);
}); });
test('should render expand/collapse all', () => {
wrapper = mountWithContexts(
<PageControls isFlatMode={false} isTemplateJob />
);
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(
<PageControls isFlatMode={false} isAllCollapsed isTemplateJob />
);
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(
<PageControls isFlatMode={false} isAllCollapsed isTemplateJob={false} />
);
const expandCollapse = wrapper.find('PageControls__ExpandCollapseWrapper');
expect(expandCollapse.find('AngleDownIcon')).toHaveLength(0);
expect(expandCollapse.find('AngleRightIcon')).toHaveLength(0);
});
}); });

View File

@@ -10,12 +10,14 @@ const initialState = {
// events with parent events that aren't yet loaded. // events with parent events that aren't yet loaded.
// arrays indexed by parent uuid // arrays indexed by parent uuid
eventsWithoutParents: {}, eventsWithoutParents: {},
isAllCollapsed: false,
}; };
export const ADD_EVENTS = 'ADD_EVENTS'; export const ADD_EVENTS = 'ADD_EVENTS';
export const TOGGLE_NODE_COLLAPSED = 'TOGGLE_NODE_COLLAPSED'; export const TOGGLE_NODE_COLLAPSED = 'TOGGLE_NODE_COLLAPSED';
export const SET_EVENT_NUM_CHILDREN = 'SET_EVENT_NUM_CHILDREN'; export const SET_EVENT_NUM_CHILDREN = 'SET_EVENT_NUM_CHILDREN';
export const CLEAR_EVENTS = 'CLEAR_EVENTS'; export const CLEAR_EVENTS = 'CLEAR_EVENTS';
export const REBUILD_TREE = 'REBUILD_TREE'; export const REBUILD_TREE = 'REBUILD_TREE';
export const TOGGLE_COLLAPSE_ALL = 'TOGGLE_COLLAPSE_ALL';
export default function useJobEvents(callbacks, isFlatMode) { export default function useJobEvents(callbacks, isFlatMode) {
const [actionQueue, setActionQueue] = useState([]); const [actionQueue, setActionQueue] = useState([]);
@@ -24,7 +26,6 @@ export default function useJobEvents(callbacks, isFlatMode) {
}; };
const reducer = jobEventsReducer(callbacks, isFlatMode, enqueueAction); const reducer = jobEventsReducer(callbacks, isFlatMode, enqueueAction);
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => { useEffect(() => {
setActionQueue((queue) => { setActionQueue((queue) => {
const action = queue[0]; const action = queue[0];
@@ -43,8 +44,10 @@ export default function useJobEvents(callbacks, isFlatMode) {
return { return {
addEvents: (events) => dispatch({ type: ADD_EVENTS, events }), addEvents: (events) => dispatch({ type: ADD_EVENTS, events }),
getNodeByUuid: (uuid) => getNodeByUuid(state, uuid), getNodeByUuid: (uuid) => getNodeByUuid(state, uuid),
toggleNodeIsCollapsed: (uuid) => toggleNodeIsCollapsed: (uuid, isCollapsed) =>
dispatch({ type: TOGGLE_NODE_COLLAPSED, uuid }), dispatch({ type: TOGGLE_NODE_COLLAPSED, uuid, isCollapsed }),
toggleCollapseAll: (isCollapsed) =>
dispatch({ type: TOGGLE_COLLAPSE_ALL, isCollapsed }),
getEventForRow: (rowIndex) => getEventForRow(state, rowIndex), getEventForRow: (rowIndex) => getEventForRow(state, rowIndex),
getNodeForRow: (rowIndex) => getNodeForRow(state, rowIndex), getNodeForRow: (rowIndex) => getNodeForRow(state, rowIndex),
getTotalNumChildren: (uuid) => { getTotalNumChildren: (uuid) => {
@@ -57,6 +60,7 @@ export default function useJobEvents(callbacks, isFlatMode) {
getEvent: (eventIndex) => getEvent(state, eventIndex), getEvent: (eventIndex) => getEvent(state, eventIndex),
clearLoadedEvents: () => dispatch({ type: CLEAR_EVENTS }), clearLoadedEvents: () => dispatch({ type: CLEAR_EVENTS }),
rebuildEventsTree: () => dispatch({ type: REBUILD_TREE }), rebuildEventsTree: () => dispatch({ type: REBUILD_TREE }),
isAllCollapsed: state.isAllCollapsed,
}; };
} }
@@ -65,6 +69,8 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) {
switch (action.type) { switch (action.type) {
case ADD_EVENTS: case ADD_EVENTS:
return addEvents(state, action.events); return addEvents(state, action.events);
case TOGGLE_COLLAPSE_ALL:
return toggleCollapseAll(state, action.isCollapsed);
case TOGGLE_NODE_COLLAPSED: case TOGGLE_NODE_COLLAPSED:
return toggleNodeIsCollapsed(state, action.uuid); return toggleNodeIsCollapsed(state, action.uuid);
case SET_EVENT_NUM_CHILDREN: case SET_EVENT_NUM_CHILDREN:
@@ -135,7 +141,7 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) {
const eventIndex = event.counter; const eventIndex = event.counter;
const newNode = { const newNode = {
eventIndex, eventIndex,
isCollapsed: false, isCollapsed: state.isAllCollapsed,
children: [], children: [],
}; };
const index = state.tree.findIndex((node) => node.eventIndex > eventIndex); const index = state.tree.findIndex((node) => node.eventIndex > eventIndex);
@@ -167,7 +173,7 @@ export function jobEventsReducer(callbacks, isFlatMode, enqueueAction) {
} }
const newNode = { const newNode = {
eventIndex, eventIndex,
isCollapsed: false, isCollapsed: state.isAllCollapsed,
children: [], children: [],
}; };
const index = parent.children.findIndex( const index = parent.children.findIndex(
@@ -400,7 +406,6 @@ function getNumCollapsedChildren(node) {
if (node.isCollapsed) { if (node.isCollapsed) {
return getTotalNumChildren(node); return getTotalNumChildren(node);
} }
let sum = 0; let sum = 0;
node.children.forEach((child) => { node.children.forEach((child) => {
sum += getNumCollapsedChildren(child); sum += getNumCollapsedChildren(child);
@@ -409,10 +414,40 @@ function getNumCollapsedChildren(node) {
} }
function toggleNodeIsCollapsed(state, eventUuid) { 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, ...node,
isCollapsed: !node.isCollapsed, isCollapsed: eventShouldNotCollapse ? false : isCollapsed,
})); children,
};
} }
function updateNodeByUuid(state, uuid, update) { function updateNodeByUuid(state, uuid, update) {

View File

@@ -167,6 +167,7 @@ describe('useJobEvents', () => {
uuidMap: {}, uuidMap: {},
eventsWithoutParents: {}, eventsWithoutParents: {},
eventGaps: [], eventGaps: [],
isAllCollapsed: false,
}; };
}); });