diff --git a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineLayout.tsx b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineLayout.tsx index 7cbf9cdc462..e547e415f24 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineLayout.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineLayout.tsx @@ -1,96 +1,121 @@ import React from 'react'; import { - Model, - ModelKind, - withPanZoom, - GraphComponent, - useComponentFactory, - useModel, - ComponentFactory, - useLayoutFactory, Graph, Layout, PipelineDagreLayout, - PipelineNodeModel, + Visualization, + VisualizationProvider, + useEventListener, + SelectionEventListener, + SELECTION_EVENT, + TopologyView, + VisualizationSurface, + useVisualizationController, + NODE_SEPARATION_HORIZONTAL, + GRAPH_LAYOUT_END_EVENT, getSpacerNodes, getEdgesFromNodes, - useVisualizationController + DEFAULT_EDGE_TYPE } from '@patternfly/react-topology'; import '@patternfly/react-styles/css/components/Topology/topology-components.css'; -import withTopologySetup from './utils/withTopologySetup'; -import pipelineComponentFactory from './components/pipelineComponentFactory'; -import { createFinallyTasks, createParallelTasks, createStatusTasks, setWhenStatus } from './utils/pipelineUtils'; +import pipelineComponentFactory, { GROUPED_EDGE_TYPE } from './components/pipelineComponentFactory'; +import { usePipelineOptions } from './usePipelineOptions'; +import { useDemoPipelineNodes } from './useDemoPipelineNodes'; +import { GROUPED_PIPELINE_NODE_SEPARATION_HORIZONTAL } from './components/DemoTaskGroupEdge'; + +export const PIPELINE_NODE_SEPARATION_VERTICAL = 65; export const LAYOUT_TITLE = 'Layout'; -const getModel = (layout: string): Model => { - const tasks: PipelineNodeModel[] = createStatusTasks('task', 4, undefined, false, true, true, true); +const PIPELINE_LAYOUT = 'PipelineLayout'; +const GROUPED_PIPELINE_LAYOUT = 'GroupedPipelineLayout'; - const whenTasks = tasks.reduce((acc, task, index) => { - if (index % (Math.floor(tasks.length / 3) + 1) !== 0) { - acc.push(task); - } - return acc; - }, []); - setWhenStatus(whenTasks); +const TopologyPipelineLayout: React.FC = () => { + const [selectedIds, setSelectedIds] = React.useState(); - for (let i = 0; i < tasks.length; i++) { - tasks[i + 1].runAfterTasks.push(tasks[i].id); - i++; - if (i + 1 < tasks.length) { - tasks[i + 1].runAfterTasks.push(tasks[i].id); - } - i++; - if (i + 1 < tasks.length) { - tasks[i + 1].runAfterTasks.push(tasks[i].id); - } - i++; - } + const controller = useVisualizationController(); + const { contextToolbar, showContextMenu, showBadges, showIcons, showGroups, badgeTooltips } = usePipelineOptions( + true + ); + const pipelineNodes = useDemoPipelineNodes( + showContextMenu, + showBadges, + showIcons, + badgeTooltips, + 'PipelineDagreLayout', + showGroups + ); - const parallelTasks = createParallelTasks('parallelTasks', tasks[9].id, 3, 2, true, true); - tasks.push(...parallelTasks); + React.useEffect(() => { + const spacerNodes = getSpacerNodes(pipelineNodes); + const nodes = [...pipelineNodes, ...spacerNodes]; + const edges = getEdgesFromNodes( + nodes.filter(n => !n.group), + showGroups ? GROUPED_EDGE_TYPE : DEFAULT_EDGE_TYPE + ); - const finallyNodes = createFinallyTasks('finally', 2, tasks, true); - const finallyGroup = { - id: 'finally-group', - type: 'finally-group', - children: finallyNodes.map(n => n.id), - group: true, - style: { padding: [35, 17] } - }; + controller.fromModel( + { + graph: { + id: 'g1', + type: 'graph', + x: 25, + y: 25, + layout: showGroups ? GROUPED_PIPELINE_LAYOUT : PIPELINE_LAYOUT + }, + nodes, + edges + }, + true + ); + controller.getGraph().layout(); + }, [controller, pipelineNodes, showGroups]); - const spacerNodes = getSpacerNodes([...tasks, ...finallyNodes]); - const edges = getEdgesFromNodes([...tasks, ...finallyNodes, ...spacerNodes]); + useEventListener(SELECTION_EVENT, ids => { + setSelectedIds(ids); + }); - return { - graph: { - id: 'g1', - type: 'graph', - x: 25, - y: 25, - layout - }, - nodes: [...tasks, ...finallyNodes, ...spacerNodes, finallyGroup], - edges - }; + return ( + + + + ); }; -export const PipelineLayout = withTopologySetup(() => { - useLayoutFactory((type: string, graph: Graph): Layout | undefined => new PipelineDagreLayout(graph, { nodesep: 95 })); - useComponentFactory(pipelineComponentFactory); - const controller = useVisualizationController(); - controller.setFitToScreenOnLayout(true); +TopologyPipelineLayout.displayName = 'TopologyPipelineLayout'; - // support pan zoom and drag - useComponentFactory( - React.useCallback(kind => { - if (kind === ModelKind.graph) { - return withPanZoom()(GraphComponent); +export const PipelineLayout = React.memo(() => { + const controller = new Visualization(); + controller.setFitToScreenOnLayout(true); + controller.registerComponentFactory(pipelineComponentFactory); + controller.registerLayoutFactory( + (type: string, graph: Graph): Layout | undefined => + new PipelineDagreLayout(graph, { + nodesep: PIPELINE_NODE_SEPARATION_VERTICAL, + ranksep: + type === GROUPED_PIPELINE_LAYOUT ? GROUPED_PIPELINE_NODE_SEPARATION_HORIZONTAL : NODE_SEPARATION_HORIZONTAL, + ignoreGroups: true + }) + ); + controller.fromModel( + { + graph: { + id: 'g1', + type: 'graph', + x: 25, + y: 25, + layout: PIPELINE_LAYOUT } - return undefined; - }, []) + }, + false ); + controller.addEventListener(GRAPH_LAYOUT_END_EVENT, () => { + controller.getGraph().fit(75); + }); - useModel(getModel('PipelineDagreLayout')); - return null; + return ( + + + + ); }); diff --git a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineTasks.tsx b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineTasks.tsx index 751b5d3df2e..451ed49833e 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineTasks.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/PipelineTasks.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Checkbox, Flex, FlexItem, Radio, ToolbarItem } from '@patternfly/react-core'; import { TopologyView, Visualization, @@ -12,150 +11,44 @@ import { } from '@patternfly/react-topology'; import '@patternfly/react-styles/css/components/Topology/topology-components.css'; import pipelineComponentFactory from './components/pipelineComponentFactory'; -import { createFinallyTasks, createStatusTasks, setWhenStatus } from './utils/pipelineUtils'; +import { usePipelineOptions } from './usePipelineOptions'; +import { useDemoPipelineNodes } from './useDemoPipelineNodes'; export const TASKS_TITLE = 'Tasks'; export const PipelineTasks: React.FC = () => { - const [showContext, setShowContext] = React.useState(false); - const [showBadges, setShowBadges] = React.useState(false); - const [showIcons, setShowIcons] = React.useState(false); const [selectedIds, setSelectedIds] = React.useState(); - const [badgeTooltips, setBadgeTooltips] = React.useState(false); const controller = useVisualizationController(); + const { contextToolbar, showContextMenu, showBadges, showIcons, badgeTooltips } = usePipelineOptions(); + const pipelineNodes = useDemoPipelineNodes(showContextMenu, showBadges, showIcons, badgeTooltips); React.useEffect(() => { - const tasks = createStatusTasks( - 'task', - 4, - undefined, - false, - false, - showContext, - showBadges, - showIcons, - badgeTooltips - ); - setWhenStatus(tasks); - const finallyNodes = createFinallyTasks('finally', 2, tasks); - const finallyGroup = { - id: 'finally-group', - type: 'finally-group', - children: finallyNodes.map(n => n.id), - group: true, - style: { padding: 30 } - }; - const model = { - graph: { - id: 'g1', - type: 'graph', - x: 25, - y: 25 + controller.fromModel( + { + graph: { + id: 'g1', + type: 'graph', + x: 25, + y: 25 + }, + nodes: pipelineNodes }, - nodes: [...tasks, ...finallyNodes, finallyGroup] - }; - controller.fromModel(model, false); - }, [badgeTooltips, controller, showBadges, showContext, showIcons]); + false + ); + }, [controller, pipelineNodes]); useEventListener(SELECTION_EVENT, ids => { setSelectedIds(ids); }); - const contextToolbar = ( - <> - - - Icons: - - checked && setShowIcons(true)} - /> - - - checked && setShowIcons(false)} - /> - - - - - - Badges: - - checked && setShowBadges(true)} - /> - - - checked && setShowBadges(false)} - /> - - - - - - Context menus: - - checked && setShowContext(true)} - /> - - - checked && setShowContext(false)} - /> - - - - - - - - ); - return ( ); }; + PipelineTasks.displayName = 'PipelineTasks'; export const TopologyPipelineTasks = React.memo(() => { diff --git a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyDemo.css b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyDemo.css index 48121dfc790..65d342ce014 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyDemo.css +++ b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyDemo.css @@ -22,10 +22,6 @@ .pf-ri__topology-demo .pf-c-toolbar__item .pf-l-flex { --pf-l-flex--FlexWrap: no-wrap; } -.pf-ri__topology-demo .pf-c-toolbar__item .pf-l-flex > * { - --pf-l-flex--spacer: 8px; -} - .pf-ri__topology-demo .pf-c-toolbar__item .pf-c-form-control { width: 85px; } diff --git a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyPackage.tsx b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyPackage.tsx index 632d4822ab7..fd73ef14fa0 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyPackage.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/TopologyPackage.tsx @@ -5,9 +5,7 @@ import { createTopologyControlButtons, defaultControlButtonsOptions, EdgeModel, - Model, NodeModel, - NodeShape, SELECTION_EVENT, SelectionEventListener, TopologyControlBar, @@ -20,43 +18,10 @@ import { VisualizationSurface } from '@patternfly/react-topology'; import stylesComponentFactory from './components/stylesComponentFactory'; -import { - Button, - Dropdown, - DropdownItem, - DropdownPosition, - DropdownToggle, - Flex, - Select, - SelectOption, - SelectVariant, - Split, - SplitItem, - TextInput, - ToolbarItem, - Tooltip -} from '@patternfly/react-core'; import defaultLayoutFactory from './layouts/defaultLayoutFactory'; import defaultComponentFactory from './components/defaultComponentFactory'; -import { - DefaultEdgeOptions, - DefaultNodeOptions, - generateDataModel, - generateEdge, - generateNode, - GeneratorEdgeOptions, - GeneratorNodeOptions -} from './data/generator'; -import { - NODE_STATUSES, - EDGE_ANIMATION_SPEEDS, - EDGE_STYLES, - EDGE_TERMINAL_TYPES, - NODE_SHAPES -} from './utils/styleUtils'; - -const GRAPH_LAYOUT_OPTIONS = ['x', 'y', 'visible', 'style', 'layout', 'scale', 'scaleExtent', 'layers']; -const NODE_LAYOUT_OPTIONS = ['x', 'y', 'visible', 'style', 'collapsed', 'width', 'height', 'shape']; +import { generateDataModel, generateEdge, generateNode } from './data/generator'; +import { useTopologyOptions } from './useTopologyOptions'; interface TopologyViewComponentProps { useSidebar: boolean; @@ -67,28 +32,21 @@ const TopologyViewComponent: React.FunctionComponent useSidebar, sideBarResizable = false }) => { - const [selectedIds, setSelectedIds] = React.useState(); - const [layoutDropdownOpen, setLayoutDropdownOpen] = React.useState(false); - const [layout, setLayout] = React.useState('ColaNoForce'); - const [nodeOptionsOpen, setNodeOptionsOpen] = React.useState(false); - const [nodeShapesOpen, setNodeShapesOpen] = React.useState(false); - const [nodeOptions, setNodeOptions] = React.useState(DefaultNodeOptions); - const [edgeOptionsOpen, setEdgeOptionsOpen] = React.useState(false); - const [edgeOptions, setEdgeOptions] = React.useState(DefaultEdgeOptions); - const [savedModel, setSavedModel] = React.useState(); - const [modelSaved, setModelSaved] = React.useState(false); - const newNodeCount = React.useRef(0); - const [numNodes, setNumNodes] = React.useState(6); - const [numEdges, setNumEdges] = React.useState(2); - const [numGroups, setNumGroups] = React.useState(1); - const [nestedLevel, setNestedLevel] = React.useState(0); - const [medScale, setMedScale] = React.useState(0.5); - const [lowScale, setLowScale] = React.useState(0.3); - const [creationCounts, setCreationCounts] = React.useState<{ numNodes: number; numEdges: number; numGroups: number }>( - { numNodes, numEdges, numGroups } - ); + const [selectedIds, setSelectedIds] = React.useState([]); const controller = useVisualizationController(); + const { + layout, + nodeOptions, + edgeOptions, + nestedLevel, + creationCounts, + medScale, + lowScale, + contextToolbar, + viewToolbar + } = useTopologyOptions(controller); + React.useEffect(() => { const dataModel = generateDataModel( creationCounts.numNodes, @@ -130,52 +88,6 @@ const TopologyViewComponent: React.FunctionComponent ); - const updateLayout = (newLayout: string) => { - setLayout(newLayout); - setLayoutDropdownOpen(false); - }; - - const layoutDropdown = ( - - - - - - setLayoutDropdownOpen(!layoutDropdownOpen)}>{layout}} - isOpen={layoutDropdownOpen} - dropdownItems={[ - updateLayout('Force')}> - Force - , - updateLayout('Dagre')}> - Dagre - , - updateLayout('Cola')}> - Cola - , - updateLayout('ColaGroups')}> - ColaGroups - , - updateLayout('ColaNoForce')}> - ColaNoForce - , - updateLayout('Grid')}> - Grid - , - updateLayout('Concentric')}> - Concentric - , - updateLayout('BreadthFirst')}> - BreadthFirst - - ]} - /> - - - ); - React.useEffect(() => { const currentModel = controller.toModel(); const nodes = currentModel.nodes; @@ -195,113 +107,6 @@ const TopologyViewComponent: React.FunctionComponent // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodeOptions]); - const renderNodeOptionsDropdown = () => { - const selectContent = ( -
- setNodeOptions(prev => ({ ...prev, nodeLabels: !prev.nodeLabels }))} - /> - setNodeOptions(prev => ({ ...prev, nodeSecondaryLabels: !prev.nodeSecondaryLabels }))} - /> - 1} - onClick={() => - setNodeOptions(prev => ({ - ...prev, - statuses: prev.statuses.length > 1 ? DefaultNodeOptions.statuses : NODE_STATUSES - })) - } - /> - - setNodeOptions(prev => ({ - ...prev, - statusDecorators: !prev.statusDecorators, - showDecorators: !prev.showDecorators - })) - } - /> - setNodeOptions(prev => ({ ...prev, nodeBadges: !prev.nodeBadges }))} - /> - setNodeOptions(prev => ({ ...prev, nodeIcons: !prev.nodeIcons }))} - /> - setNodeOptions(prev => ({ ...prev, contextMenus: !prev.contextMenus }))} - /> -
- ); - - return ( - setNodeShapesOpen(prev => !prev)} - onSelect={() => {}} - isCheckboxSelectionBadgeHidden - isOpen={nodeShapesOpen} - placeholderText="Node shapes" - /> - ); - }; - React.useEffect(() => { const currentModel = controller.toModel(); const edges = currentModel.edges; @@ -314,259 +119,6 @@ const TopologyViewComponent: React.FunctionComponent } }, [edgeOptions, controller]); - const renderEdgeOptionsDropdown = () => { - const selectContent = ( -
- 1} - onClick={() => - setEdgeOptions(prev => ({ - ...prev, - edgeStatuses: prev.edgeStatuses.length > 1 ? DefaultEdgeOptions.edgeStatuses : NODE_STATUSES - })) - } - /> - 1} - onClick={() => - setEdgeOptions(prev => ({ - ...prev, - edgeStyles: prev.edgeStyles.length > 1 ? DefaultEdgeOptions.edgeStyles : EDGE_STYLES - })) - } - /> - 1} - onClick={() => - setEdgeOptions(prev => ({ - ...prev, - edgeAnimations: prev.edgeAnimations.length > 1 ? DefaultEdgeOptions.edgeAnimations : EDGE_ANIMATION_SPEEDS - })) - } - /> - 1} - onClick={() => - setEdgeOptions(prev => ({ - ...prev, - terminalTypes: prev.terminalTypes.length > 1 ? DefaultEdgeOptions.terminalTypes : EDGE_TERMINAL_TYPES - })) - } - /> - setEdgeOptions(prev => ({ ...prev, edgeTags: !prev.edgeTags }))} - /> -
- ); - - return ( - setNodeOptionsOpen(prev => !prev)} + onSelect={() => {}} + isCheckboxSelectionBadgeHidden + isOpen={nodeOptionsOpen} + placeholderText="Node options" + /> + ); + }; + + const toggleNodeShape = (shape: NodeShape): void => { + const index = nodeOptions.shapes.indexOf(shape); + if (index >= 0) { + setNodeOptions(prev => ({ + ...prev, + shapes: [...prev.shapes.slice(0, index), ...prev.shapes.slice(index + 1)] + })); + } else { + setNodeOptions(prev => ({ + ...prev, + shapes: [...prev.shapes, shape] + })); + } + }; + + const renderNodeShapesDropdown = () => { + const selectContent = ( +
+ {NODE_SHAPES.map(shape => ( + toggleNodeShape(shape)} + /> + ))} +
+ ); + + return ( + setEdgeOptionsOpen(prev => !prev)} + onSelect={() => {}} + isCheckboxSelectionBadgeHidden + isOpen={edgeOptionsOpen} + placeholderText="Edge options" + /> + ); + }; + + const saveModel = () => { + setSavedModel(controller.toModel()); + setModelSaved(true); + window.setTimeout(() => { + setModelSaved(false); + }, 2000); + }; + + const restoreLayout = () => { + if (savedModel) { + const currentModel = controller.toModel(); + currentModel.graph = { + ...currentModel.graph, + ..._.pick(savedModel.graph, GRAPH_LAYOUT_OPTIONS) + }; + currentModel.nodes = currentModel.nodes.map(n => { + const savedNode = savedModel.nodes.find(sn => sn.id === n.id); + if (!savedNode) { + return n; + } + return { + ...n, + ..._.pick(savedNode, NODE_LAYOUT_OPTIONS) + }; + }); + controller.fromModel(currentModel, false); + + if (savedModel.graph.layout !== layout) { + setLayout(savedModel.graph.layout); + } + } + }; + + const addNode = () => { + const newNode = { + id: `new-node-${newNodeCount.current++}`, + type: 'node', + width: 100, + height: 100, + data: {} + }; + const currentModel = controller.toModel(); + currentModel.nodes.push(newNode); + controller.fromModel(currentModel); + }; + + const updateValue = (value: number, min: number, max: number, setter: any): void => { + if (value >= min && value <= max) { + setter(value); + } + }; + + const contextToolbar = ( + <> + + + Nodes: + (val ? updateValue(parseInt(val), 0, 9999, setNumNodes) : setNumNodes(null))} + /> + Edges: + (val ? updateValue(parseInt(val), 0, 200, setNumEdges) : setNumEdges(null))} + /> + Groups: + (val ? updateValue(parseInt(val), 0, 100, setNumGroups) : setNumGroups(null))} + /> + Nesting Depth: + (val ? updateValue(parseInt(val), 0, 5, setNestedLevel) : setNestedLevel(null))} + /> + + + + {renderNodeOptionsDropdown()} + {renderNodeShapesDropdown()} + {renderEdgeOptionsDropdown()} + + ); + + const viewToolbar = ( + <> + {layoutDropdown} + + + + + + + + + + + + + + Medium Scale: + { + const newValue = parseFloat(val); + if (!Number.isNaN(newValue) && newValue > lowScale && newValue >= 0.01 && newValue <= 1.0) { + setMedScale(parseFloat(val)); + } + }} + /> + Low Scale: + { + const newValue = parseFloat(val); + if (!Number.isNaN(newValue) && newValue < medScale && newValue >= 0.01 && newValue <= 1.0) { + setLowScale(parseFloat(val)); + } + }} + /> + + + + ); + + return { + layout, + nodeOptions, + edgeOptions, + nestedLevel, + creationCounts, + medScale, + lowScale, + contextToolbar, + viewToolbar + }; +}; diff --git a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/utils/pipelineUtils.ts b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/utils/pipelineUtils.ts deleted file mode 100644 index 646ff79e787..00000000000 --- a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/utils/pipelineUtils.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { - DEFAULT_FINALLY_NODE_TYPE, - DEFAULT_TASK_NODE_TYPE, - PipelineNodeModel, - RunStatus, - WhenStatus, - DEFAULT_WHEN_OFFSET, - DEFAULT_WHEN_SIZE -} from '@patternfly/react-topology'; -import { logos } from './logos'; - -export const ROW_HEIGHT = 100; -export const COLUMN_WIDTH = 250; - -export const DEFAULT_TASK_WIDTH = 170; -export const FINALLY_TASK_WIDTH = DEFAULT_TASK_WIDTH - DEFAULT_WHEN_OFFSET - DEFAULT_WHEN_SIZE; -export const DEFAULT_TASK_HEIGHT = 32; - -export const TASK_STATUSES = [ - undefined, - RunStatus.Succeeded, - RunStatus.Failed, - RunStatus.Running, - RunStatus.InProgress, - RunStatus.FailedToStart, - RunStatus.Skipped, - RunStatus.Cancelled, - RunStatus.Pending, - RunStatus.Idle -]; - -export const createTask = (options: { - id: string; - label?: string; - status?: RunStatus; - selected?: boolean; - row?: number; - column?: number; - width?: number; - height?: number; - x?: number; - y?: number; - marginX?: number; - marginY?: number; - noLocation?: boolean; - showContextMenu?: boolean; - showBadge?: boolean; - showIcon?: boolean; - badgeTooltips?: boolean; -}): PipelineNodeModel => { - const width = options.width || DEFAULT_TASK_WIDTH; - const height = options.height || DEFAULT_TASK_HEIGHT; - const columnWidth = COLUMN_WIDTH + (options.showIcon ? 28 : 0); - const x = - options.x !== undefined - ? options.x - : (options.marginX ?? +(options.showIcon ? 28 : 0)) + (options.column ?? 0) * columnWidth; - const y = options.y !== undefined ? options.y : (options.marginY ?? 40) + (options.row ?? 0) * ROW_HEIGHT; - const taskIconOptions = { - taskIconClass: options.showIcon ? logos.get('icon-java') : undefined, - taskIconTooltip: options.showIcon ? 'Environment' : undefined - }; - return { - id: options.id, - type: DEFAULT_TASK_NODE_TYPE, - label: options.label, - width, - height, - style: { paddingLeft: DEFAULT_WHEN_SIZE + DEFAULT_WHEN_OFFSET }, - x: options.noLocation ? undefined : x, - y: options.noLocation ? undefined : y, - runAfterTasks: [], - data: { - status, - badge: options.showBadge ? '3/4' : undefined, - ...taskIconOptions, - ...options - } - }; -}; - -export const createStatusTasks = ( - baseId: string, - statusPerRow = 1, - label: string = '', - selected?: boolean, - noLocation?: boolean, - showContextMenu?: boolean, - showBadge?: boolean, - showIcon?: boolean, - badgeTooltips?: boolean -): PipelineNodeModel[] => - TASK_STATUSES.map((status, index) => - createTask({ - id: `${baseId}-${status}`, - status, - label: `${label ? `${label} ` : ''}${status || 'No status'} Task`, - row: Math.ceil((index + 1) / statusPerRow) - 1, - column: index % statusPerRow, - selected, - noLocation, - showContextMenu, - showBadge, - showIcon, - badgeTooltips, - width: DEFAULT_TASK_WIDTH + (showContextMenu ? 10 : 0) + (showBadge ? 40 : 0) - }) - ); - -export const setWhenStatus = (tasks: PipelineNodeModel[]): void => { - tasks.forEach((task, index) => { - task.data.whenStatus = index % 2 === 0 ? WhenStatus.Met : WhenStatus.Unmet; - }); -}; - -export const createFinallyTask = (options: { - id: string; - label?: string; - status?: RunStatus; - selected?: boolean; - width?: number; - height?: number; - x: number; - y: number; - noLocation?: boolean; -}): PipelineNodeModel => { - const width = options.width || FINALLY_TASK_WIDTH; - const height = options.height || DEFAULT_TASK_HEIGHT; - const x = options.x; - const y = options.y; - return { - id: options.id, - type: DEFAULT_FINALLY_NODE_TYPE, - label: options.label, - width, - height, - style: { paddingLeft: DEFAULT_WHEN_SIZE + DEFAULT_WHEN_OFFSET }, - x: options.noLocation ? undefined : x, - y: options.noLocation ? undefined : y, - data: { - ...options - } - }; -}; - -export const createFinallyTasks = ( - baseId: string, - count = 2, - tasks: PipelineNodeModel[], - noLocation?: boolean -): PipelineNodeModel[] => { - const maxX = noLocation ? 0 : Math.max(...tasks.map(t => t.x)); - const maxY = noLocation ? 0 : Math.max(...tasks.map(t => t.y)) + DEFAULT_TASK_HEIGHT; - const x = maxX + COLUMN_WIDTH; - const startY = maxY / 2 - (ROW_HEIGHT * (count - 1)) / 2; - - const nodes = []; - for (let i = 0; i < count; i++) { - const node = createFinallyTask({ - id: `finally-${baseId}-${i}`, - label: `Finally ${baseId}`, - noLocation, - x: noLocation ? undefined : x, - y: noLocation ? undefined : startY + ROW_HEIGHT * i - }); - nodes.push(node); - } - return nodes; -}; - -export const createParallelTasks = ( - baseId: string, - runAfterId: string, - taskCount = 3, - taskDepth = 2, - showContextMenu?: boolean, - showBadge?: boolean -): PipelineNodeModel[] => { - const nodes: PipelineNodeModel[] = []; - - for (let i = 0; i < taskCount; i++) { - nodes.push( - createTask({ - id: `${baseId}-task-${i}`, - status: RunStatus.Pending, - label: `${baseId} Sub-Task ${i}`, - noLocation: true, - width: DEFAULT_TASK_WIDTH + (showContextMenu ? 10 : 0) + (showBadge ? 40 : 0) - }) - ); - } - let usedNodes = 0; - while (usedNodes < nodes.length) { - for (let depth = 0; depth < taskDepth; depth++) { - if (usedNodes < nodes.length) { - if (depth === 0) { - nodes[usedNodes].runAfterTasks = [runAfterId]; - } else { - nodes[usedNodes].runAfterTasks = [nodes[usedNodes - 1].id]; - } - } - usedNodes++; - } - } - return nodes; -}; diff --git a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/utils/styleUtils.ts b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/utils/styleUtils.ts index 487c233c86a..2b0c4a6a088 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/utils/styleUtils.ts +++ b/packages/react-integration/demo-app-ts/src/components/demos/TopologyDemo/utils/styleUtils.ts @@ -126,6 +126,7 @@ export const createNode = (options: { shape, status: options.status || NodeStatus.default, labelPosition: options.labelPosition, + // data items are used to pass to the component to show various option, demo purposes only data: { dataType: 'Default', ...options @@ -166,6 +167,7 @@ export const createEdge = ( target: targetId, edgeStyle: options.style, animationSpeed: options.animation, + // data items are used to pass to the component to show various option, demo purposes only data: { endTerminalType: options.terminalType, endTerminalStatus: options.terminalStatus, diff --git a/packages/react-topology/src/layouts/DagreLayout.ts b/packages/react-topology/src/layouts/DagreLayout.ts index 00d243e8b26..ecab6c47b8f 100644 --- a/packages/react-topology/src/layouts/DagreLayout.ts +++ b/packages/react-topology/src/layouts/DagreLayout.ts @@ -9,10 +9,10 @@ import { DagreNode } from './DagreNode'; import { DagreGroup } from './DagreGroup'; import { DagreLink } from './DagreLink'; -export type DagreLayoutOptions = LayoutOptions & dagre.GraphLabel; +export type DagreLayoutOptions = LayoutOptions & dagre.GraphLabel & { ignoreGroups?: boolean }; export class DagreLayout extends BaseLayout implements Layout { - private dagreOptions: DagreLayoutOptions; + protected dagreOptions: DagreLayoutOptions; constructor(graph: Graph, options?: Partial) { super(graph, options); @@ -57,17 +57,21 @@ export class DagreLayout extends BaseLayout implements Layout { const dagreGraph = new dagre.graphlib.Graph({ compound: true }); dagreGraph.setGraph(_.omit(this.dagreOptions, Object.keys(LAYOUT_DEFAULTS))); - _.forEach(this.groups, group => { - dagreGraph.setNode(group.id, group); - dagreGraph.setParent(group.id, group.element.getParent().getId()); - }); + if (!this.dagreOptions.ignoreGroups) { + _.forEach(this.groups, group => { + dagreGraph.setNode(group.id, group); + dagreGraph.setParent(group.id, group.element.getParent().getId()); + }); + } const updatedNodes: dagre.Node[] = []; _.forEach(this.nodes, node => { const updateNode = (node as DagreNode).getUpdatableNode(); updatedNodes.push(updateNode); dagreGraph.setNode(node.id, updateNode); - dagreGraph.setParent(node.id, node.element.getParent().getId()); + if (!this.dagreOptions.ignoreGroups) { + dagreGraph.setParent(node.id, node.element.getParent().getId()); + } }); _.forEach(this.edges, dagreEdge => { diff --git a/packages/react-topology/src/pipelines/components/edges/TaskEdge.tsx b/packages/react-topology/src/pipelines/components/edges/TaskEdge.tsx index 544ca44115d..9aa8e11844d 100644 --- a/packages/react-topology/src/pipelines/components/edges/TaskEdge.tsx +++ b/packages/react-topology/src/pipelines/components/edges/TaskEdge.tsx @@ -1,16 +1,17 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Edge } from '../../../types'; -import { integralShapePath } from '../../utils/draw-utils'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Topology/topology-components'; +import { Edge } from '../../../types'; +import { integralShapePath } from '../../utils'; interface TaskEdgeProps { element: Edge; className?: string; + nodeSeparation?: number; } -const TaskEdge: React.FunctionComponent = ({ element, className }) => { +const TaskEdge: React.FunctionComponent = ({ element, className, nodeSeparation }) => { const startPoint = element.getStartPoint(); const endPoint = element.getEndPoint(); const groupClassName = css(styles.topologyEdge, className); @@ -19,7 +20,7 @@ const TaskEdge: React.FunctionComponent = ({ element, className } return ( diff --git a/packages/react-topology/src/pipelines/components/groups/DefaultTaskGroup.tsx b/packages/react-topology/src/pipelines/components/groups/DefaultTaskGroup.tsx index b82fb05d12f..bfef43f92f1 100644 --- a/packages/react-topology/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/react-topology/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -6,8 +6,8 @@ import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-alt-ic import NodeLabel from '../../../components/nodes/labels/NodeLabel'; import { Layer } from '../../../components/layers'; import { GROUPS_LAYER } from '../../../const'; -import { maxPadding, useCombineRefs, useHover } from '../../../utils'; -import { BadgeLocation, isGraph, Node, NodeStyle } from '../../../types'; +import { useCombineRefs, useHover } from '../../../utils'; +import { BadgeLocation, isGraph, Node } from '../../../types'; import { useDragNode, WithContextMenuProps, @@ -81,25 +81,16 @@ const DefaultTaskGroup: React.FunctionComponent = ({ parent = parent.getParent(); } - // cast to number and coerce - const padding = maxPadding(element.getStyle().padding ?? 17); - const children = element.getNodes().filter(c => c.isVisible()); if (children.length === 0) { return null; } - const { minX, minY, maxX, maxY } = children.reduce( - (acc, child) => { - const bounds = child.getBounds(); - return { - minX: Math.min(acc.minX, bounds.x - padding), - minY: Math.min(acc.minY, bounds.y - padding), - maxX: Math.max(acc.maxX, bounds.x + bounds.width + padding), - maxY: Math.max(acc.maxY, bounds.y + bounds.height + padding) - }; - }, - { minX: Infinity, minY: Infinity, maxX: 0, maxY: 0 } - ); + + const bounds = element.getBounds(); + const minX = bounds.x; + const minY = bounds.y; + const maxX = bounds.x + bounds.width; + const maxY = bounds.y + bounds.height; const groupClassName = css( styles.topologyGroup, diff --git a/packages/react-topology/src/pipelines/components/nodes/TaskNode.tsx b/packages/react-topology/src/pipelines/components/nodes/TaskNode.tsx index e91da3e40ba..dcd36435a71 100644 --- a/packages/react-topology/src/pipelines/components/nodes/TaskNode.tsx +++ b/packages/react-topology/src/pipelines/components/nodes/TaskNode.tsx @@ -12,7 +12,6 @@ import { createSvgIdUrl, getNodeScaleTranslation, useCombineRefs, useHover, useS import { getRunStatusModifier, nonShadowModifiers } from '../../utils'; import StatusIcon from '../../utils/StatusIcon'; import { TaskNodeSourceAnchor, TaskNodeTargetAnchor } from '../anchors'; -import { DEFAULT_WHEN_OFFSET, DEFAULT_WHEN_SIZE } from '../../decorators/WhenDecorator'; import LabelActionIcon from '../../../components/nodes/labels/LabelActionIcon'; import LabelContextMenu from '../../../components/nodes/labels/LabelContextMenu'; import NodeShadows, { @@ -97,8 +96,8 @@ const TaskNode: React.FC }> = selected, onSelect, hasWhenExpression = false, - whenSize = DEFAULT_WHEN_SIZE, - whenOffset = DEFAULT_WHEN_OFFSET, + whenSize = 0, + whenOffset = 0, onContextMenu, contextMenuOpen, actionIcon, diff --git a/packages/react-topology/src/pipelines/layouts/PipelineDagreLayout.ts b/packages/react-topology/src/pipelines/layouts/PipelineDagreLayout.ts index bf8a66c4e12..a6321cbc7e6 100644 --- a/packages/react-topology/src/pipelines/layouts/PipelineDagreLayout.ts +++ b/packages/react-topology/src/pipelines/layouts/PipelineDagreLayout.ts @@ -23,4 +23,10 @@ export class PipelineDagreLayout extends DagreLayout implements Layout { ...options }); } + set nodesep(nodesep: number) { + super.dagreOptions.nodesep = nodesep; + } + set ranksep(ranksep: number) { + super.dagreOptions.ranksep = ranksep; + } } diff --git a/packages/react-topology/src/pipelines/utils/draw-utils.ts b/packages/react-topology/src/pipelines/utils/draw-utils.ts index b54eb97002f..923f3ccab05 100644 --- a/packages/react-topology/src/pipelines/utils/draw-utils.ts +++ b/packages/react-topology/src/pipelines/utils/draw-utils.ts @@ -2,7 +2,7 @@ import { Point } from '../../geom'; import { DrawDesign, NODE_SEPARATION_HORIZONTAL } from '../const'; type SingleDraw = (p: Point) => string; -type DoubleDraw = (p1: Point, p2: Point, startIndentX?: number) => string; +type DoubleDraw = (p1: Point, p2: Point, startIndentX?: number, junctionOffset?: number) => string; type TripleDraw = (p1: Point, p2: Point, p3: Point) => string; type DetermineDirection = (p1: Point, p2: Point) => boolean; @@ -59,13 +59,18 @@ const curve: TripleDraw = (fromPoint, cornerPoint, toPoint) => { export const straightPath: DoubleDraw = (start, finish) => join(moveTo(start), lineTo(finish)); -export const integralShapePath: DoubleDraw = (start, finish, startIndentX = 0) => { +export const integralShapePath: DoubleDraw = ( + start, + finish, + startIndentX = 0, + nodeSeparation = NODE_SEPARATION_HORIZONTAL +) => { // Integral shape: ∫ let firstCurve: string = null; let secondCurve: string = null; if (start.y !== finish.y) { - const cornerX = Math.floor(start.x + NODE_SEPARATION_HORIZONTAL / 2); + const cornerX = Math.floor(start.x + nodeSeparation / 2); const firstCorner = new Point(cornerX, start.y); const secondCorner = new Point(cornerX, finish.y); diff --git a/packages/react-topology/src/pipelines/utils/utils.ts b/packages/react-topology/src/pipelines/utils/utils.ts index 09e89c8aa29..8465d86447e 100644 --- a/packages/react-topology/src/pipelines/utils/utils.ts +++ b/packages/react-topology/src/pipelines/utils/utils.ts @@ -103,7 +103,9 @@ export const getSpacerNodes = ( const finallyId = getSpacerId(finallyNodes.map(n => n.id)); spacerNodes.push({ id: finallyId, - type: spacerNodeType + type: spacerNodeType, + width: 1, + height: 1 }); } @@ -112,11 +114,9 @@ export const getSpacerNodes = ( export const getEdgesFromNodes = ( nodes: PipelineNodeModel[], - spacerNodeType = DEFAULT_SPACER_NODE_TYPE, edgeType = DEFAULT_EDGE_TYPE, - spacerEdgeType = DEFAULT_EDGE_TYPE, - finallyNodeTypes: string[] = [DEFAULT_FINALLY_NODE_TYPE], - finallyEdgeType = DEFAULT_EDGE_TYPE + spacerNodeType = DEFAULT_SPACER_NODE_TYPE, + finallyNodeTypes: string[] = [DEFAULT_FINALLY_NODE_TYPE] ): EdgeModel[] => { const edges: EdgeModel[] = []; @@ -135,7 +135,7 @@ export const getEdgesFromNodes = ( if (node && !finallyNodes.includes(node)) { edges.push({ id: `${sourceId}-${spacer.id}`, - type: spacerEdgeType, + type: edgeType, source: sourceId, target: spacer.id }); @@ -150,7 +150,7 @@ export const getEdgesFromNodes = ( if (spacer) { edges.push({ id: `${spacer.id}-${node.id}`, - type: spacerEdgeType, + type: edgeType, source: spacer.id, target: node.id }); @@ -172,7 +172,7 @@ export const getEdgesFromNodes = ( finallyNodes.forEach(finallyNode => { edges.push({ id: `${finallyId}-${finallyNode.id}`, - type: spacerEdgeType, + type: edgeType, source: finallyId, target: finallyNode.id }); @@ -180,7 +180,7 @@ export const getEdgesFromNodes = ( lastTasks.forEach(lastTaskNode => { edges.push({ id: `${lastTaskNode.id}-${finallyId}`, - type: spacerEdgeType, + type: edgeType, source: lastTaskNode.id, target: finallyId }); @@ -190,7 +190,7 @@ export const getEdgesFromNodes = ( lastTasks.forEach(lastTaskNode => { edges.push({ id: `finallyId-${lastTaskNode.id}-${finallyNodes[0].id}`, - type: finallyEdgeType, + type: edgeType, source: lastTaskNode.id, target: finallyNodes[0].id });