mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 15:36:04 -03:30
Merge pull request #11517 from AlexSCorey/11236-ExpandCollapseAll
Adds expand collapse all functionality on job output page.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ describe('useJobEvents', () => {
|
|||||||
uuidMap: {},
|
uuidMap: {},
|
||||||
eventsWithoutParents: {},
|
eventsWithoutParents: {},
|
||||||
eventGaps: [],
|
eventGaps: [],
|
||||||
|
isAllCollapsed: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user