diff --git a/packages/demo-app-ts/src/demos/DemoControlBar.tsx b/packages/demo-app-ts/src/demos/DemoControlBar.tsx index c252d247..8a6eeecf 100644 --- a/packages/demo-app-ts/src/demos/DemoControlBar.tsx +++ b/packages/demo-app-ts/src/demos/DemoControlBar.tsx @@ -7,13 +7,17 @@ import { action } from '@patternfly/react-topology'; -const DemoControlBar: React.FC = () => { +const DemoControlBar: React.FC<{ collapseAllCallback?: (collapseAll: boolean) => void }> = ({ + collapseAllCallback +}) => { const controller = useVisualizationController(); return ( { controller.getGraph().scaleBy(4 / 3); }), @@ -27,6 +31,12 @@ const DemoControlBar: React.FC = () => { controller.getGraph().reset(); controller.getGraph().layout(); }), + expandAllCallback: action(() => { + collapseAllCallback(false); + }), + collapseAllCallback: action(() => { + collapseAllCallback(true); + }), legend: false })} /> diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx index 5df8931a..ff9e27ec 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx @@ -25,7 +25,12 @@ import { Edge } from '@patternfly/react-topology'; import pipelineGroupsComponentFactory from './pipelineGroupsComponentFactory'; -import { createComplexDemoPipelineGroupsNodes, createDemoPipelineGroupsNodes } from './createDemoPipelineGroupsNodes'; +import { + createComplexDemoPipelineGroupsNodes, + createDemoPipelineGroupsNodes, + DEFAULT_TASK_HEIGHT, + GROUP_TASK_WIDTH +} from './createDemoPipelineGroupsNodes'; import { PipelineGroupsDemoContext, PipelineGroupsDemoModel } from './PipelineGroupsDemoContext'; import OptionsBar from './OptionsBar'; import DemoControlBar from '../DemoControlBar'; @@ -78,8 +83,48 @@ const TopologyPipelineGroups: React.FC<{ nodes: PipelineNodeModel[] }> = observe ); }, [controller, nodes, options.verticalLayout]); + const collapseAllCallback = React.useCallback( + (collapseAll: boolean) => { + // First, expand/collapse all nodes + collapseAll ? controller.getGraph().collapseAll() : controller.getGraph().expandAll(); + // We must recreate the model based on what is visible + const model = controller.toModel(); + + // Get all the non-spacer nodes, mark them all visible again + const nodes = model.nodes + .filter((n) => n.type !== DEFAULT_SPACER_NODE_TYPE) + .map((n) => ({ + ...n, + visible: true + })); + + // If collapsing, set the size of the collapsed group nodes + if (collapseAll) { + nodes.forEach((node) => { + if (node.group && node.collapsed) { + node.width = GROUP_TASK_WIDTH; + node.height = DEFAULT_TASK_HEIGHT; + } + }); + } + // Determine the new set of nodes, including the spacer nodes + const pipelineNodes = addSpacerNodes(nodes); + + // Determine the new edges + const edges = getEdgesFromNodes(pipelineNodes, DEFAULT_SPACER_NODE_TYPE, 'edge', 'edge'); + // Apply the new model and run the layout + controller.fromModel({ nodes: pipelineNodes, edges }, true); + controller.getGraph().layout(); + controller.getGraph().fit(80); + }, + [controller] + ); + return ( - } controlBar={}> + } + controlBar={} + > ); diff --git a/packages/module/src/components/TopologyControlBar/TopologyControlBar.tsx b/packages/module/src/components/TopologyControlBar/TopologyControlBar.tsx index 6897f52e..a9e169e6 100644 --- a/packages/module/src/components/TopologyControlBar/TopologyControlBar.tsx +++ b/packages/module/src/components/TopologyControlBar/TopologyControlBar.tsx @@ -12,6 +12,9 @@ import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; import ExpandArrowsAltIcon from '@patternfly/react-icons/dist/esm/icons/expand-arrows-alt-icon'; import SearchPlusIcon from '@patternfly/react-icons/dist/esm/icons/search-plus-icon'; import SearchMinusIcon from '@patternfly/react-icons/dist/esm/icons/search-minus-icon'; +import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-alt-icon'; +import ExpandAltIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; + import '../../css/topology-controlbar'; /* ID's for common control buttons */ @@ -19,6 +22,8 @@ export const ZOOM_IN = 'zoom-in'; export const ZOOM_OUT = 'zoom-out'; export const FIT_TO_SCREEN = 'fit-to-screen'; export const RESET_VIEW = 'reset-view'; +export const EXPAND_ALL = 'expand-all'; +export const COLLAPSE_ALL = 'collapse-all'; export const LEGEND = 'legend'; /* Data needed for each control button */ @@ -66,6 +71,22 @@ export interface TopologyControlButtonsOptions { resetViewDisabled: boolean; resetViewHidden: boolean; + expandAll: boolean; + expandAllIcon: React.ReactNode; + expandAllTip: React.ReactNode; + expandAllAriaLabel: string; + expandAllCallback: (id: any) => void; + expandAllDisabled: boolean; + expandAllHidden: boolean; + + collapseAll: boolean; + collapseAllIcon: React.ReactNode; + collapseAllTip: React.ReactNode; + collapseAllAriaLabel: string; + collapseAllCallback: (id: any) => void; + collapseAllDisabled: boolean; + collapseAllHidden: boolean; + legend: boolean; legendIcon: React.ReactNode; legendTip: string; @@ -111,6 +132,22 @@ export const defaultControlButtonsOptions: TopologyControlButtonsOptions = { resetViewDisabled: false, resetViewHidden: false, + expandAll: false, + expandAllIcon: , + expandAllTip: 'Expand All', + expandAllAriaLabel: 'Expand All', + expandAllCallback: null, + expandAllDisabled: false, + expandAllHidden: false, + + collapseAll: false, + collapseAllIcon: , + collapseAllTip: 'Collapse All', + collapseAllAriaLabel: 'Collapse All', + collapseAllCallback: null, + collapseAllDisabled: false, + collapseAllHidden: false, + legend: true, legendIcon: 'Legend', legendTip: '', @@ -156,6 +193,22 @@ export const createTopologyControlButtons = ({ resetViewDisabled = defaultControlButtonsOptions.resetViewDisabled, resetViewHidden = defaultControlButtonsOptions.resetViewHidden, + expandAll = defaultControlButtonsOptions.expandAll, + expandAllIcon = defaultControlButtonsOptions.expandAllIcon, + expandAllTip = defaultControlButtonsOptions.expandAllTip, + expandAllAriaLabel = defaultControlButtonsOptions.expandAllAriaLabel, + expandAllCallback = defaultControlButtonsOptions.expandAllCallback, + expandAllDisabled = defaultControlButtonsOptions.expandAllDisabled, + expandAllHidden = defaultControlButtonsOptions.expandAllHidden, + + collapseAll = defaultControlButtonsOptions.collapseAll, + collapseAllIcon = defaultControlButtonsOptions.collapseAllIcon, + collapseAllTip = defaultControlButtonsOptions.collapseAllTip, + collapseAllAriaLabel = defaultControlButtonsOptions.collapseAllAriaLabel, + collapseAllCallback = defaultControlButtonsOptions.collapseAllCallback, + collapseAllDisabled = defaultControlButtonsOptions.collapseAllDisabled, + collapseAllHidden = defaultControlButtonsOptions.collapseAllHidden, + legend = defaultControlButtonsOptions.legend, legendIcon = defaultControlButtonsOptions.legendIcon, legendTip = defaultControlButtonsOptions.legendTip, @@ -216,6 +269,30 @@ export const createTopologyControlButtons = ({ }); } + if (expandAll) { + controlButtons.push({ + id: EXPAND_ALL, + icon: expandAllIcon, + tooltip: expandAllTip, + ariaLabel: expandAllAriaLabel, + callback: expandAllCallback, + disabled: expandAllDisabled, + hidden: expandAllHidden + }); + } + + if (collapseAll) { + controlButtons.push({ + id: COLLAPSE_ALL, + icon: collapseAllIcon, + tooltip: collapseAllTip, + ariaLabel: collapseAllAriaLabel, + callback: collapseAllCallback, + disabled: collapseAllDisabled, + hidden: collapseAllHidden + }); + } + if (customButtons) { controlButtons.push(...customButtons); } diff --git a/packages/module/src/elements/BaseGraph.ts b/packages/module/src/elements/BaseGraph.ts index 3b6e8b10..3ac4804f 100644 --- a/packages/module/src/elements/BaseGraph.ts +++ b/packages/module/src/elements/BaseGraph.ts @@ -226,6 +226,34 @@ export default class BaseGraph this.setPosition(new Point(0, 0)); } + setAllChildrenCollapsedState(parent: Node, collapsed: boolean): void { + // eslint-disable-next-line no-console + console.log(parent.getAllNodeChildren(false)); + parent.getAllNodeChildren(false).forEach((node) => { + if (node.isGroup()) { + node.setCollapsed(collapsed); + } + }); + } + + expandAll(): void { + this.getNodes().forEach((node) => { + if (node.isGroup()) { + node.setCollapsed(false); + this.setAllChildrenCollapsedState(node, false); + } + }); + } + + collapseAll(): void { + this.getNodes().forEach((node) => { + if (node.isGroup()) { + node.setCollapsed(true); + this.setAllChildrenCollapsedState(node, true); + } + }); + } + scaleBy(scale: number, location?: Point): void { const b = this.getBounds(); let { x, y } = b; diff --git a/packages/module/src/elements/BaseNode.ts b/packages/module/src/elements/BaseNode.ts index 1966c220..f059de0f 100644 --- a/packages/module/src/elements/BaseNode.ts +++ b/packages/module/src/elements/BaseNode.ts @@ -157,12 +157,15 @@ export default class BaseNode return super.getChildren(); } - // Return all child leaf nodes regardless of collapse status or child groups' collapsed status - getAllNodeChildren(): Node[] { + // Return all child nodes regardless of collapse status or child groups' collapsed status + getAllNodeChildren(leafOnly: boolean = true): Node[] { return super.getChildren().reduce((total, nexChild) => { if (isNode(nexChild)) { if (nexChild.isGroup()) { - return total.concat(nexChild.getAllNodeChildren()); + total.push(...nexChild.getAllNodeChildren(leafOnly)); + if (leafOnly) { + return total; + } } total.push(nexChild); } diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx index 6f998c97..69f5d0ea 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -9,6 +9,7 @@ import { getEdgesFromNodes, getSpacerNodes } from '../../utils'; import DefaultTaskGroupCollapsed from './DefaultTaskGroupCollapsed'; import DefaultTaskGroupExpanded from './DefaultTaskGroupExpanded'; import { RunStatus } from '../../types'; +import { DEFAULT_SPACER_NODE_TYPE } from '../../const'; export interface EdgeCreationTypes { spacerNodeType?: string; @@ -145,7 +146,7 @@ const DefaultTaskGroupInner: React.FunctionComponent n.type !== creationTypes.spacerNodeType) + .filter((n) => n.type !== (creationTypes.spacerNodeType || DEFAULT_SPACER_NODE_TYPE)) .map((n) => ({ ...n, visible: true diff --git a/packages/module/src/types.ts b/packages/module/src/types.ts index 5841c0b9..68cf7ab6 100644 --- a/packages/module/src/types.ts +++ b/packages/module/src/types.ts @@ -232,7 +232,7 @@ export interface Node extends GraphEle setNodeStatus(shape: NodeStatus): void; getSourceEdges(): Edge[]; getTargetEdges(): Edge[]; - getAllNodeChildren(): Node[]; // Return all children regardless of collapse status or child groups' collapsed status + getAllNodeChildren(leafOnly?: boolean): Node[]; // Return all children regardless of collapse status or child groups' collapsed status getPositionableChildren(): Node[]; // Return all children that can be positioned (collapsed groups are positionable) isDimensionsInitialized(): boolean; isPositioned(): boolean; @@ -287,6 +287,8 @@ export interface Graph extends Graph fit(padding?: number): void; panIntoView(element: Node, options?: { offset?: number; minimumVisible?: number }): void; isNodeInView(element: Node, options?: { padding: number }): boolean; + expandAll(): void; + collapseAll(): void; } export const isGraph = (element: GraphElement): element is Graph => element && element.getKind() === ModelKind.graph;