From 69d6270ae63b48fa441479dd0ef647138dc7819c Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Tue, 13 Feb 2024 08:36:19 -0500 Subject: [PATCH] chore(demo): Update demo to show a better example --- packages/demo-app-ts/src/Demo.css | 2 +- packages/demo-app-ts/src/Demos.ts | 8 +- .../{DefaultEdge.tsx => DemoDefaultEdge.tsx} | 4 +- ...{DefaultGroup.tsx => DemoDefaultGroup.tsx} | 4 +- .../src/components/defaultComponentFactory.ts | 4 +- packages/demo-app-ts/src/data/generator.ts | 233 -------- .../src/demos/CollapsibleGroups.tsx | 2 +- packages/demo-app-ts/src/demos/Connectors.tsx | 2 +- packages/demo-app-ts/src/demos/Groups.tsx | 2 +- packages/demo-app-ts/src/demos/Layouts.tsx | 8 +- .../demo-app-ts/src/demos/TopologyPackage.tsx | 278 --------- .../pipelinesDemo}/DemoFinallyNode.tsx | 0 .../pipelinesDemo}/DemoTaskGroupEdge.tsx | 0 .../pipelinesDemo}/DemoTaskNode.tsx | 45 +- .../pipelinesDemo/PipelineDemoContext.tsx | 80 +++ .../{ => pipelinesDemo}/PipelineLayout.tsx | 43 +- .../pipelinesDemo/PipelineOptionsBar.tsx | 67 +++ .../{ => pipelinesDemo}/PipelineTasks.tsx | 26 +- .../TopologyPipelineDemo.tsx | 0 .../pipelineComponentFactory.tsx | 0 .../pipelinesDemo}/useDemoPipelineNodes.tsx | 38 +- .../statusConnectorsDemo}/FailedEdge.tsx | 0 .../StatusConnectorNode.tsx | 0 .../StatusConnectors.tsx | 72 ++- .../statusConnectorsDemo}/SuccessEdge.tsx | 0 .../statusConnectorsComponentFactory.ts | 2 +- .../useSourceStatusAnchor.tsx | 0 .../useTargetStatusAnchor.tsx | 0 .../stylesDemo}/StyleEdge.tsx | 0 .../stylesDemo}/StyleGroup.tsx | 0 .../stylesDemo}/StyleNode.tsx | 0 .../src/demos/{ => stylesDemo}/Styles.tsx | 14 +- .../{utils => demos/stylesDemo}/styleUtils.ts | 39 +- .../stylesDemo}/stylesComponentFactory.tsx | 4 +- .../demos/topologyPackageDemo/DemoContext.tsx | 121 ++++ .../demos/topologyPackageDemo/DemoEdge.tsx | 41 ++ .../demos/topologyPackageDemo/DemoGroup.tsx | 71 +++ .../demos/topologyPackageDemo/DemoNode.tsx | 206 +++++++ .../topologyPackageDemo/OptionsContextBar.tsx | 288 +++++++++ .../topologyPackageDemo/OptionsViewBar.tsx | 201 +++++++ .../topologyPackageDemo/TopologyPackage.tsx | 162 +++++ .../demoComponentFactory.tsx | 143 +++++ .../demos/topologyPackageDemo/generator.ts | 192 ++++++ .../demos/topologyPackageDemo/listeners.ts | 45 ++ packages/demo-app-ts/src/index.tsx | 1 + .../src/utils/usePipelineOptions.tsx | 50 -- .../src/utils/useTopologyOptions.tsx | 555 ------------------ .../src/components/edges/DefaultEdge.tsx | 16 +- .../src/components/groups/DefaultGroup.tsx | 2 + 49 files changed, 1790 insertions(+), 1281 deletions(-) rename packages/demo-app-ts/src/components/{DefaultEdge.tsx => DemoDefaultEdge.tsx} (95%) rename packages/demo-app-ts/src/components/{DefaultGroup.tsx => DemoDefaultGroup.tsx} (94%) delete mode 100644 packages/demo-app-ts/src/data/generator.ts delete mode 100644 packages/demo-app-ts/src/demos/TopologyPackage.tsx rename packages/demo-app-ts/src/{components => demos/pipelinesDemo}/DemoFinallyNode.tsx (100%) rename packages/demo-app-ts/src/{components => demos/pipelinesDemo}/DemoTaskGroupEdge.tsx (100%) rename packages/demo-app-ts/src/{components => demos/pipelinesDemo}/DemoTaskNode.tsx (54%) create mode 100644 packages/demo-app-ts/src/demos/pipelinesDemo/PipelineDemoContext.tsx rename packages/demo-app-ts/src/demos/{ => pipelinesDemo}/PipelineLayout.tsx (68%) create mode 100644 packages/demo-app-ts/src/demos/pipelinesDemo/PipelineOptionsBar.tsx rename packages/demo-app-ts/src/demos/{ => pipelinesDemo}/PipelineTasks.tsx (61%) rename packages/demo-app-ts/src/demos/{ => pipelinesDemo}/TopologyPipelineDemo.tsx (100%) rename packages/demo-app-ts/src/{components => demos/pipelinesDemo}/pipelineComponentFactory.tsx (100%) rename packages/demo-app-ts/src/{utils => demos/pipelinesDemo}/useDemoPipelineNodes.tsx (87%) rename packages/demo-app-ts/src/{components => demos/statusConnectorsDemo}/FailedEdge.tsx (100%) rename packages/demo-app-ts/src/{components => demos/statusConnectorsDemo}/StatusConnectorNode.tsx (100%) rename packages/demo-app-ts/src/demos/{ => statusConnectorsDemo}/StatusConnectors.tsx (81%) rename packages/demo-app-ts/src/{components => demos/statusConnectorsDemo}/SuccessEdge.tsx (100%) rename packages/demo-app-ts/src/{components => demos/statusConnectorsDemo}/statusConnectorsComponentFactory.ts (93%) rename packages/demo-app-ts/src/{components => demos/statusConnectorsDemo}/useSourceStatusAnchor.tsx (100%) rename packages/demo-app-ts/src/{components => demos/statusConnectorsDemo}/useTargetStatusAnchor.tsx (100%) rename packages/demo-app-ts/src/{components => demos/stylesDemo}/StyleEdge.tsx (100%) rename packages/demo-app-ts/src/{components => demos/stylesDemo}/StyleGroup.tsx (100%) rename packages/demo-app-ts/src/{components => demos/stylesDemo}/StyleNode.tsx (100%) rename packages/demo-app-ts/src/demos/{ => stylesDemo}/Styles.tsx (98%) rename packages/demo-app-ts/src/{utils => demos/stylesDemo}/styleUtils.ts (92%) rename packages/demo-app-ts/src/{components => demos/stylesDemo}/stylesComponentFactory.tsx (97%) create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/DemoGroup.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsViewBar.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/TopologyPackage.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/demoComponentFactory.tsx create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts create mode 100644 packages/demo-app-ts/src/demos/topologyPackageDemo/listeners.ts delete mode 100644 packages/demo-app-ts/src/utils/usePipelineOptions.tsx delete mode 100644 packages/demo-app-ts/src/utils/useTopologyOptions.tsx diff --git a/packages/demo-app-ts/src/Demo.css b/packages/demo-app-ts/src/Demo.css index a89db665..2dd72142 100644 --- a/packages/demo-app-ts/src/Demo.css +++ b/packages/demo-app-ts/src/Demo.css @@ -28,7 +28,7 @@ --pf-v5-l-flex--FlexWrap: no-wrap; } .pf-ri__topology-demo .pf-v5-c-toolbar__item .pf-v5-c-form-control { - width: 85px; + width: 70px; } .pf-ri-topology-context-menu { z-index: 1050; diff --git a/packages/demo-app-ts/src/Demos.ts b/packages/demo-app-ts/src/Demos.ts index fb3ac5a7..75a5b3df 100644 --- a/packages/demo-app-ts/src/Demos.ts +++ b/packages/demo-app-ts/src/Demos.ts @@ -1,6 +1,6 @@ -import { TopologyPipelineDemo } from './demos/TopologyPipelineDemo'; +import { TopologyPipelineDemo } from './demos/pipelinesDemo/TopologyPipelineDemo'; import { Basics } from './demos/Basics'; -import { StyleEdges, StyleGroups, StyleLabels, StyleNodes } from './demos/Styles'; +import { StyleEdges, StyleGroups, StyleLabels, StyleNodes } from './demos/stylesDemo/Styles'; import { Selection } from './demos/Selection'; import { PanZoom } from './demos/PanZoom'; import { Layouts } from './demos/Layouts'; @@ -8,10 +8,10 @@ import { Connectors } from './demos/Connectors'; import { DragAndDrop } from './demos/DragDrop'; import { Shapes } from './demos/Shapes'; import { ContextMenus } from './demos/ContextMenus'; -import { TopologyPackage } from './demos/TopologyPackage'; +import { TopologyPackage } from './demos/topologyPackageDemo/TopologyPackage'; import { ComplexGroup } from './demos/Groups'; import { CollapsibleGroups } from './demos/CollapsibleGroups'; -import { StatusConnectors } from './demos/StatusConnectors'; +import { StatusConnectors } from './demos/statusConnectorsDemo/StatusConnectors'; import './Demo.css'; diff --git a/packages/demo-app-ts/src/components/DefaultEdge.tsx b/packages/demo-app-ts/src/components/DemoDefaultEdge.tsx similarity index 95% rename from packages/demo-app-ts/src/components/DefaultEdge.tsx rename to packages/demo-app-ts/src/components/DemoDefaultEdge.tsx index 1c7cf87c..858396c1 100644 --- a/packages/demo-app-ts/src/components/DefaultEdge.tsx +++ b/packages/demo-app-ts/src/components/DemoDefaultEdge.tsx @@ -41,7 +41,7 @@ const Bendpoint: React.FunctionComponent = observer(({ point }) ); }); -const DefaultEdge: React.FunctionComponent = ({ +const DemoDefaultEdge: React.FunctionComponent = ({ element, sourceDragRef, targetDragRef, @@ -76,4 +76,4 @@ const DefaultEdge: React.FunctionComponent = ({ ); }; -export default observer(DefaultEdge); +export default observer(DemoDefaultEdge); diff --git a/packages/demo-app-ts/src/components/DefaultGroup.tsx b/packages/demo-app-ts/src/components/DemoDefaultGroup.tsx similarity index 94% rename from packages/demo-app-ts/src/components/DefaultGroup.tsx rename to packages/demo-app-ts/src/components/DemoDefaultGroup.tsx index 328075b6..fbee8b4a 100644 --- a/packages/demo-app-ts/src/components/DefaultGroup.tsx +++ b/packages/demo-app-ts/src/components/DemoDefaultGroup.tsx @@ -25,7 +25,7 @@ type GroupProps = { WithDndDragProps & WithDndDropProps; -const DefaultGroup: React.FunctionComponent = ({ +const DemoDefaultGroup: React.FunctionComponent = ({ element, children, selected, @@ -93,4 +93,4 @@ const DefaultGroup: React.FunctionComponent = ({ ); }; -export default observer(DefaultGroup); +export default observer(DemoDefaultGroup); diff --git a/packages/demo-app-ts/src/components/defaultComponentFactory.ts b/packages/demo-app-ts/src/components/defaultComponentFactory.ts index 5deae228..b862d01f 100644 --- a/packages/demo-app-ts/src/components/defaultComponentFactory.ts +++ b/packages/demo-app-ts/src/components/defaultComponentFactory.ts @@ -1,8 +1,8 @@ import { ComponentType } from 'react'; import { GraphElement, ComponentFactory, ModelKind, GraphComponent, DefaultNode } from '@patternfly/react-topology'; -import Edge from './DefaultEdge'; +import Edge from './DemoDefaultEdge'; import MultiEdge from './MultiEdge'; -import Group from './DefaultGroup'; +import Group from './DemoDefaultGroup'; import GroupHull from './GroupHull'; const defaultComponentFactory: ComponentFactory = ( diff --git a/packages/demo-app-ts/src/data/generator.ts b/packages/demo-app-ts/src/data/generator.ts deleted file mode 100644 index eca3bdbc..00000000 --- a/packages/demo-app-ts/src/data/generator.ts +++ /dev/null @@ -1,233 +0,0 @@ -import React from 'react'; -import { - EdgeAnimationSpeed, - EdgeModel, - EdgeStyle, - EdgeTerminalType, - Model, - NodeModel, - NodeShape, - NodeStatus -} from '@patternfly/react-topology'; -import SignOutAltIcon from '@patternfly/react-icons/dist/esm/icons/skull-icon'; -import { createEdge, createNode } from '../utils/styleUtils'; -import { logos } from '../utils/logos'; -import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon'; - -const getRandomNode = (numNodes: number, notNode = -1): number => { - let node = Math.floor(Math.random() * numNodes); - if (node === notNode) { - node = getRandomNode(numNodes, notNode); - } - return node; -}; - -export interface GeneratorNodeOptions { - shapes?: NodeShape[]; - statuses?: NodeStatus[]; - statusDecorators?: boolean; - showDecorators?: boolean; - nodeLabels?: boolean; - nodeSecondaryLabels?: boolean; - nodeBadges?: boolean; - nodeIcons?: boolean; - smallNodes?: boolean; - contextMenus?: boolean; - hulledOutline?: boolean; -} - -export interface GeneratorEdgeOptions { - edgeStyles?: EdgeStyle[]; - edgeStatuses?: NodeStatus[]; - edgeAnimations?: EdgeAnimationSpeed[]; - edgeTags?: boolean; - terminalTypes?: EdgeTerminalType[]; -} - -export const DefaultNodeOptions: GeneratorNodeOptions = { - shapes: [NodeShape.ellipse], - statuses: [NodeStatus.default], - statusDecorators: false, - showDecorators: false, - nodeLabels: true, - nodeSecondaryLabels: false, - nodeBadges: false, - nodeIcons: false, - smallNodes: false, - contextMenus: false, - hulledOutline: true -}; - -export const DefaultEdgeOptions: GeneratorEdgeOptions = { - edgeStyles: [EdgeStyle.default], - edgeStatuses: [NodeStatus.default], - edgeAnimations: [EdgeAnimationSpeed.none], - edgeTags: false, - terminalTypes: [EdgeTerminalType.directional] -}; - -export const getNodeOptions = ( - index: number, - nodeCreationOptions: GeneratorNodeOptions -): { - status?: NodeStatus; - shape?: NodeShape; - label?: string; - secondaryLabel?: string; - badge?: string; - showStatusDecorator?: boolean; - showDecorators?: boolean; - showContextMenu?: boolean; - hulledOutline?: boolean; - labelIconClass?: string; - labelIcon?: React.ComponentClass; -} => { - const shapeEnumIndex = Math.round(Math.random() * (nodeCreationOptions.shapes.length - 1)); - const labelIconClass = index % 2 === 0 && nodeCreationOptions.nodeIcons ? logos.get('icon-java') : undefined; - const labelIcon = index % 2 === 1 && nodeCreationOptions.nodeIcons ? SignOutAltIcon : undefined; - return { - status: nodeCreationOptions.statuses[index % nodeCreationOptions.statuses.length], - shape: nodeCreationOptions.shapes[shapeEnumIndex], - label: nodeCreationOptions.nodeLabels ? `Node ${index} Title` : undefined, - secondaryLabel: nodeCreationOptions.nodeSecondaryLabels ? `Node subtitle` : undefined, - badge: nodeCreationOptions.nodeBadges ? 'CS' : undefined, - showStatusDecorator: nodeCreationOptions.statusDecorators, - showDecorators: nodeCreationOptions.showDecorators, - showContextMenu: nodeCreationOptions.contextMenus, - hulledOutline: nodeCreationOptions.hulledOutline, - labelIconClass, - labelIcon - }; -}; - -export const generateNode = (index: number, nodeCreationOptions: GeneratorNodeOptions): NodeModel => { - const nodeId = `node-${index}`; - const width = nodeCreationOptions.smallNodes ? 48 : 75; - let height = nodeCreationOptions.smallNodes ? 48 : 75; - - const nodeOptions = getNodeOptions(index, nodeCreationOptions); - if (nodeOptions.shape === NodeShape.stadium) { - height *= 0.5; - } - return createNode({ - id: nodeId, - width, - height, - setLocation: false, - ...nodeOptions - }); -}; - -export const generateEdge = ( - index: number, - sourceId: string, - targetId: string, - options: GeneratorEdgeOptions -): EdgeModel => - createEdge(sourceId, targetId, { - style: options.edgeStyles[index % options.edgeStyles.length], - animation: options.edgeAnimations[index % options.edgeAnimations.length], - terminalType: options.terminalTypes[index % options.terminalTypes.length], - terminalStatus: options.edgeStatuses[index % options.edgeStatuses.length], - tag: options.edgeTags ? '250kbs' : undefined, - tagStatus: options.edgeStatuses[index % options.edgeStatuses.length] - }); - -export const updateGroup = (group: NodeModel, nodeCreationOptions: GeneratorNodeOptions): NodeModel => { - return { - ...group, - data: { - badge: nodeCreationOptions.nodeBadges ? 'GN' : undefined, - badgeColor: '#F2F0FC', - badgeTextColor: '#5752d1', - badgeBorderColor: '#CBC1FF', - collapsedWidth: 75, - collapsedHeight: 75, - showContextMenu: nodeCreationOptions.contextMenus, - collapsible: true, - hulledOutline: nodeCreationOptions.hulledOutline - } - }; -}; - -export const generateDataModel = ( - numNodes: number, - numGroups: number, - numEdges: number, - groupDepth: number = 0, - nodeOptions: GeneratorNodeOptions = {}, - edgeOptions: GeneratorEdgeOptions = {} -): Model => { - const nodeCreationOptions = { ...DefaultNodeOptions, ...nodeOptions }; - const edgeCreationOptions = { ...DefaultEdgeOptions, ...edgeOptions }; - - const groups: NodeModel[] = []; - const nodes: NodeModel[] = []; - const edges: EdgeModel[] = []; - - const createGroup = ( - childNodes: NodeModel[], - baseId: string = 'Group', - index: number, - level: number = 0 - ): NodeModel => { - const id = `${baseId}-${index}`; - const group: NodeModel = { - id, - children: [], - type: 'group', - group: true, - label: id, - style: { padding: 15 }, - // data items are used to pass to the component to show various option, demo purposes only - data: { - badge: nodeCreationOptions.nodeBadges ? 'GN' : undefined, - badgeColor: '#F2F0FC', - badgeTextColor: '#5752d1', - badgeBorderColor: '#CBC1FF', - collapsedWidth: 75, - collapsedHeight: 75, - showContextMenu: nodeCreationOptions.contextMenus, - collapsible: true, - hulledOutline: nodeOptions.hulledOutline - } - }; - if (level === groupDepth) { - group.children = childNodes.map(n => n.id); - } else { - const nodesPerChildGroup = Math.floor(childNodes.length / 2); - if (nodesPerChildGroup < 1) { - const g1 = createGroup(childNodes, id, 1, level + 1); - group.children = [g1.id]; - } else { - const g1 = createGroup(childNodes.slice(0, nodesPerChildGroup), id, 1, level + 1); - const g2 = createGroup(childNodes.slice(nodesPerChildGroup), id, 2, level + 1); - group.children = [g1.id, g2.id]; - } - } - - groups.push(group); - return group; - }; - - for (let i = 0; i < numNodes; i++) { - const node = generateNode(i, nodeCreationOptions); - nodes.push(node); - } - - const nodesPerGroup = Math.floor((numNodes - 2) / numGroups); - for (let i = 0; i < numGroups; i++) { - createGroup(nodes.slice(i * nodesPerGroup, (i + 1) * nodesPerGroup), 'Group', i + 1); - } - - for (let i = 0; i < numEdges; i++) { - const sourceNum = getRandomNode(numNodes); - const targetNum = getRandomNode(numNodes, sourceNum); - const edge = generateEdge(i, nodes[sourceNum].id, nodes[targetNum].id, edgeCreationOptions); - edges.push(edge); - } - - nodes.push(...groups); - - return { nodes, edges }; -}; diff --git a/packages/demo-app-ts/src/demos/CollapsibleGroups.tsx b/packages/demo-app-ts/src/demos/CollapsibleGroups.tsx index 640c1daa..e629eb74 100644 --- a/packages/demo-app-ts/src/demos/CollapsibleGroups.tsx +++ b/packages/demo-app-ts/src/demos/CollapsibleGroups.tsx @@ -25,7 +25,7 @@ import { ToolbarGroup, ToolbarItem, Checkbox } from '@patternfly/react-core'; import defaultLayoutFactory from '../layouts/defaultLayoutFactory'; import data from '../data/group-types'; import GroupHull from '../components/GroupHull'; -import Group from '../components/DefaultGroup'; +import Group from '../components/DemoDefaultGroup'; import DemoDefaultNode from '../components/DemoDefaultNode'; import defaultComponentFactory from '../components/defaultComponentFactory'; diff --git a/packages/demo-app-ts/src/demos/Connectors.tsx b/packages/demo-app-ts/src/demos/Connectors.tsx index ddab741d..fbd1b42c 100644 --- a/packages/demo-app-ts/src/demos/Connectors.tsx +++ b/packages/demo-app-ts/src/demos/Connectors.tsx @@ -29,7 +29,7 @@ import { GraphElementProps } from '@patternfly/react-topology'; import defaultComponentFactory from '../components/defaultComponentFactory'; -import DefaultEdge from '../components/DefaultEdge'; +import DefaultEdge from '../components/DemoDefaultEdge'; import DemoDefaultNode from '../components/DemoDefaultNode'; import withTopologySetup from '../utils/withTopologySetup'; import NodeRect from '../components/NodeRect'; diff --git a/packages/demo-app-ts/src/demos/Groups.tsx b/packages/demo-app-ts/src/demos/Groups.tsx index 290173b3..0beeb143 100644 --- a/packages/demo-app-ts/src/demos/Groups.tsx +++ b/packages/demo-app-ts/src/demos/Groups.tsx @@ -15,7 +15,7 @@ import { GraphElement } from '@patternfly/react-topology'; import defaultComponentFactory from '../components/defaultComponentFactory'; -import DefaultGroup from '../components/DefaultGroup'; +import DefaultGroup from '../components/DemoDefaultGroup'; import DemoDefaultNode from '../components/DemoDefaultNode'; import defaultLayoutFactory from '../layouts/defaultLayoutFactory'; import withTopologySetup from '../utils/withTopologySetup'; diff --git a/packages/demo-app-ts/src/demos/Layouts.tsx b/packages/demo-app-ts/src/demos/Layouts.tsx index edea98ab..53b06f13 100644 --- a/packages/demo-app-ts/src/demos/Layouts.tsx +++ b/packages/demo-app-ts/src/demos/Layouts.tsx @@ -13,16 +13,16 @@ import { import defaultLayoutFactory from '../layouts/defaultLayoutFactory'; import defaultComponentFactory from '../components/defaultComponentFactory'; import GroupHull from '../components/GroupHull'; -import Group from '../components/DefaultGroup'; +import Group from '../components/DemoDefaultGroup'; import DemoDefaultNode from '../components/DemoDefaultNode'; import withTopologySetup from '../utils/withTopologySetup'; -import { generateDataModel } from '../data/generator'; -import stylesComponentFactory from '../components/stylesComponentFactory'; +import { generateDataModel } from './topologyPackageDemo/generator'; +import stylesComponentFactory from './stylesDemo/stylesComponentFactory'; import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; const getModel = (layout: string): Model => { // create nodes from data - const model = generateDataModel(200, 5, 20); + const model = generateDataModel(200, 5, 20, 0); model.graph = { id: 'g1', type: 'graph', diff --git a/packages/demo-app-ts/src/demos/TopologyPackage.tsx b/packages/demo-app-ts/src/demos/TopologyPackage.tsx deleted file mode 100644 index d11e11a0..00000000 --- a/packages/demo-app-ts/src/demos/TopologyPackage.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import * as React from 'react'; -import { action } from 'mobx'; -import { - Controller, - createTopologyControlButtons, - defaultControlButtonsOptions, - EdgeModel, - EventListener, - GRAPH_POSITION_CHANGE_EVENT, - GRAPH_LAYOUT_END_EVENT, - isNode, - Node, - NodeModel, - SELECTION_EVENT, - SelectionEventListener, - TopologyControlBar, - TopologySideBar, - TopologyView, - useEventListener, - useVisualizationController, - Visualization, - VisualizationProvider, - VisualizationSurface -} from '@patternfly/react-topology'; -import stylesComponentFactory from '../components/stylesComponentFactory'; -import defaultLayoutFactory from '../layouts/defaultLayoutFactory'; -import defaultComponentFactory from '../components/defaultComponentFactory'; -import { generateDataModel, generateEdge, generateNode, updateGroup } from '../data/generator'; -import { useTopologyOptions } from '../utils/useTopologyOptions'; -import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; - -interface TopologyViewComponentProps { - useSidebar: boolean; - sideBarResizable?: boolean; -} - -let positionTimer: NodeJS.Timer; - -const graphPositionChangeListener: EventListener = ({ graph }): void => { - const scale = graph.getScale(); - const position = graph.getPosition(); - const scaleExtent = graph.getScaleExtent(); - - // eslint-disable-next-line no-console - console.log(`Graph Position Change:\n Position: ${Math.round(position.x)},${Math.round(position.y)}\n Scale: ${scale}\n Scale Extent: max: ${scaleExtent[0]} max: ${scaleExtent[1]}`); - - // After an interval, check that what we got was the final value. - if (positionTimer) { - clearTimeout(positionTimer); - } - - positionTimer = setTimeout(() => { - const newScale = graph.getScale(); - const newPosition = graph.getPosition(); - const newScaleExtent = graph.getScaleExtent(); - - // Output an error if any of the graph position values differ from when the last event was fired - if (newScale !== scale) { - // eslint-disable-next-line no-console - console.error(`Scale Changed: ${scale} => ${newScale}`); - } - if (newPosition.x !== position.x || newPosition.y !== position.y) { - // eslint-disable-next-line no-console - console.error(`Graph Position Changed: ${Math.round(position.x)},${Math.round(position.y)} => ${Math.round(newPosition.x)},${Math.round(newPosition.y)}`); - } - if (newScaleExtent !== scaleExtent) { - // eslint-disable-next-line no-console - console.error(`Scale Extent Changed: ${scaleExtent} => ${scaleExtent}`); - } - }, 1000); -}; - -const layoutEndListener: EventListener = ({ graph }): void => { - const controller: Controller = graph.getController(); - const positions = controller.getElements().filter(e => isNode(e)).map((node) => `Node: ${node.getLabel()}: ${Math.round((node as Node).getPosition().x)},${Math.round((node as Node).getPosition().y)}`); - - // eslint-disable-next-line no-console - console.log(`Layout Complete:\n${positions.join('\n')}`); -}; - - -const TopologyViewComponent: React.FunctionComponent = ({ - useSidebar, - sideBarResizable = false -}) => { - 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, - creationCounts.numGroups, - creationCounts.numEdges, - nestedLevel, - nodeOptions, - edgeOptions - ); - - const model = { - graph: { - id: 'g1', - type: 'graph', - layout - }, - ...dataModel - }; - - controller.fromModel(model, false); - // Don't update on option changes, its handled differently to not re-layout - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [creationCounts, layout]); - - useEventListener(SELECTION_EVENT, ids => { - setSelectedIds(ids); - }); - - React.useEffect(() => { - - controller.addEventListener(GRAPH_POSITION_CHANGE_EVENT, graphPositionChangeListener); - controller.addEventListener(GRAPH_LAYOUT_END_EVENT, layoutEndListener); - - return () => { - controller.removeEventListener(GRAPH_POSITION_CHANGE_EVENT, graphPositionChangeListener); - controller.removeEventListener(GRAPH_LAYOUT_END_EVENT, layoutEndListener); - }; - }, [controller]); - - React.useEffect(() => { - controller.getGraph().setDetailsLevelThresholds({ - low: lowScale, - medium: medScale - }); - }, [controller, lowScale, medScale]); - - const topologySideBar = ( - setSelectedIds([])}> -
{selectedIds?.[0]}
-
- ); - - React.useEffect(() => { - const currentModel = controller.toModel(); - const nodes = currentModel.nodes; - if (nodes.length) { - const updatedNodes: NodeModel[] = nodes.map((node, index) => { - if (node.group) { - return updateGroup(node, nodeOptions); - } - return { - ...node, - ...generateNode(index, nodeOptions) - }; - }); - controller.fromModel({ nodes: updatedNodes, edges: currentModel.edges }); - } - // Don't update on controller change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nodeOptions]); - - React.useEffect(() => { - const currentModel = controller.toModel(); - const edges = currentModel.edges; - if (edges.length) { - const updatedEdges: EdgeModel[] = edges.map((edge, index) => ({ - ...edge, - ...generateEdge(index, edge.source, edge.target, edgeOptions) - })); - controller.fromModel({ edges: updatedEdges, nodes: currentModel.nodes }); - } - }, [edgeOptions, controller]); - - return ( - { - controller.getGraph().scaleBy(4 / 3); - }), - zoomOutCallback: action(() => { - controller.getGraph().scaleBy(0.75); - }), - fitToScreenCallback: action(() => { - controller.getGraph().fit(80); - }), - resetViewCallback: action(() => { - controller.getGraph().reset(); - controller.getGraph().layout(); - }), - legend: false - })} - /> - } - contextToolbar={contextToolbar} - viewToolbar={viewToolbar} - sideBar={useSidebar && topologySideBar} - sideBarOpen={useSidebar && !!selectedIds?.length} - sideBarResizable={sideBarResizable} - > - - - ); -}; - -export const Topology = React.memo(() => { - const controller = new Visualization(); - controller.registerLayoutFactory(defaultLayoutFactory); - controller.registerComponentFactory(defaultComponentFactory); - controller.registerComponentFactory(stylesComponentFactory); - - return ( - - - - ); -}); - -export const WithSideBar = React.memo(() => { - const controller = new Visualization(); - controller.registerLayoutFactory(defaultLayoutFactory); - controller.registerComponentFactory(defaultComponentFactory); - controller.registerComponentFactory(stylesComponentFactory); - - return ( - - - - ); -}); - -export const WithResizableSideBar = React.memo(() => { - const controller = new Visualization(); - controller.registerLayoutFactory(defaultLayoutFactory); - controller.registerComponentFactory(defaultComponentFactory); - controller.registerComponentFactory(stylesComponentFactory); - return ( - - - - ); -}); - -export const TopologyPackage: React.FunctionComponent = () => { - const [activeKey, setActiveKey] = React.useState(0); - - const handleTabClick = (_event: React.MouseEvent, tabIndex: string | number) => { - setActiveKey(tabIndex); - }; - - return ( -
- - Topology}> - - - With Side Bar}> - - - With Resizeable Side Bar}> - - - -
- ); -}; diff --git a/packages/demo-app-ts/src/components/DemoFinallyNode.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoFinallyNode.tsx similarity index 100% rename from packages/demo-app-ts/src/components/DemoFinallyNode.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/DemoFinallyNode.tsx diff --git a/packages/demo-app-ts/src/components/DemoTaskGroupEdge.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskGroupEdge.tsx similarity index 100% rename from packages/demo-app-ts/src/components/DemoTaskGroupEdge.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskGroupEdge.tsx diff --git a/packages/demo-app-ts/src/components/DemoTaskNode.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskNode.tsx similarity index 54% rename from packages/demo-app-ts/src/components/DemoTaskNode.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskNode.tsx index 03f7925a..b65815df 100644 --- a/packages/demo-app-ts/src/components/DemoTaskNode.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskNode.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react'; import { DEFAULT_LAYER, DEFAULT_WHEN_OFFSET, + DEFAULT_WHEN_SIZE, GraphElement, Layer, Node, @@ -15,6 +16,10 @@ import { WithSelectionProps } from '@patternfly/react-topology'; import { PopoverProps } from '@patternfly/react-core'; +import { CubeIcon } from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; +import { logos } from '../../utils/logos'; +import { PipelineDemoContext } from './PipelineDemoContext'; type DemoTaskNodeProps = { element: GraphElement; @@ -23,33 +28,34 @@ type DemoTaskNodeProps = { const DEMO_TIP_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id feugiat augue, nec fringilla turpis.'; +const getLeadIcon = (taskJobType: string) => { + switch (taskJobType) { + case 'cubes': + return ; + case 'link': + return ; + default: + return null; + } +}; + const DemoTaskNode: React.FunctionComponent = ({ element, onContextMenu, contextMenuOpen, ...rest }) => { + const pipelineOptions = React.useContext(PipelineDemoContext); const nodeElement = element as Node; const data = element.getData(); const [hover, hoverRef] = useHover(); const detailsLevel = element.getGraph().getDetailsLevel(); - const passedData = React.useMemo(() => { - const newData = { ...data }; - Object.keys(newData).forEach(key => { - if (newData[key] === undefined) { - delete newData[key]; - } - }); - return newData; - }, [data]); - - const hasTaskIcon = !!(data.taskIconClass || data.taskIcon); const whenDecorator = data.whenStatus ? ( ) : null; @@ -65,14 +71,21 @@ const DemoTaskNode: React.FunctionComponent = ({ {whenDecorator} diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineDemoContext.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineDemoContext.tsx new file mode 100644 index 00000000..25819ebc --- /dev/null +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineDemoContext.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { makeObservable, observable, action } from 'mobx'; + +export class PipelineDemoModel { + protected showIconsP: boolean = false; + protected showBadgesP: boolean = false; + protected showBadgeTooltipsP: boolean = false; + protected showContextMenusP: boolean = false; + protected showGroupsP: boolean = false; + protected verticalLayoutP: boolean = false; + + constructor() { + makeObservable< + PipelineDemoModel, + | 'showIconsP' + | 'showBadgesP' + | 'showBadgeTooltipsP' + | 'showContextMenusP' + | 'showGroupsP' + | 'verticalLayoutP' + >(this, { + showIconsP: observable, + showBadgesP: observable, + showBadgeTooltipsP: observable, + showContextMenusP: observable, + showGroupsP: observable, + verticalLayoutP: observable, + setShowIcons: action, + setShowBadges: action, + setShowBadgeTooltips: action, + setShowContextMenus: action, + setShowGroups: action, + setVerticalLayout: action, + }); + } + + public get showIcons(): boolean { + return this.showIconsP; + } + public setShowIcons = (show: boolean): void => { + this.showIconsP = show; + } + + public get showBadges(): boolean { + return this.showBadgesP; + } + public setShowBadges = (show: boolean): void => { + this.showBadgesP = show; + } + + public get showBadgeTooltips(): boolean { + return this.showBadgeTooltipsP; + } + public setShowBadgeTooltips = (show: boolean): void => { + this.showBadgeTooltipsP = show; + } + + public get showContextMenus(): boolean { + return this.showContextMenusP; + } + public setShowContextMenus = (show: boolean): void => { + this.showContextMenusP = show; + } + + public get showGroups(): boolean { + return this.showGroupsP; + } + public setShowGroups = (show: boolean): void => { + this.showGroupsP = show; + } + + public get verticalLayout(): boolean { + return this.verticalLayoutP; + } + public setVerticalLayout = (show: boolean): void => { + this.verticalLayoutP = show; + } +} + +export const PipelineDemoContext = React.createContext(new PipelineDemoModel()); \ No newline at end of file diff --git a/packages/demo-app-ts/src/demos/PipelineLayout.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineLayout.tsx similarity index 68% rename from packages/demo-app-ts/src/demos/PipelineLayout.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/PipelineLayout.tsx index 19096ffe..53209f8f 100644 --- a/packages/demo-app-ts/src/demos/PipelineLayout.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineLayout.tsx @@ -19,12 +19,14 @@ import { DEFAULT_SPACER_NODE_TYPE, DEFAULT_FINALLY_NODE_TYPE, TOP_TO_BOTTOM, - LEFT_TO_RIGHT + LEFT_TO_RIGHT, + observer } from '@patternfly/react-topology'; -import pipelineComponentFactory, { GROUPED_EDGE_TYPE } from '../components/pipelineComponentFactory'; -import { usePipelineOptions } from '../utils/usePipelineOptions'; -import { useDemoPipelineNodes } from '../utils/useDemoPipelineNodes'; -import { GROUPED_PIPELINE_NODE_SEPARATION_HORIZONTAL } from '../components/DemoTaskGroupEdge'; +import pipelineComponentFactory, { GROUPED_EDGE_TYPE } from './pipelineComponentFactory'; +import { useDemoPipelineNodes } from './useDemoPipelineNodes'; +import { GROUPED_PIPELINE_NODE_SEPARATION_HORIZONTAL } from './DemoTaskGroupEdge'; +import { PipelineDemoContext, PipelineDemoModel } from './PipelineDemoContext'; +import PipelineOptionsBar from './PipelineOptionsBar'; export const PIPELINE_NODE_SEPARATION_VERTICAL = 65; @@ -34,26 +36,23 @@ const GROUP_PREFIX = 'Grouped_'; const VERTICAL_SUFFIX = '_Vertical'; const PIPELINE_LAYOUT = 'PipelineLayout'; -const TopologyPipelineLayout: React.FC = () => { +const TopologyPipelineLayout: React.FC = observer(() => { const [selectedIds, setSelectedIds] = React.useState(); const controller = useVisualizationController(); - const { contextToolbar, showContextMenu, showBadges, showIcons, showGroups, badgeTooltips, verticalLayout } = usePipelineOptions( - true - ); + const pipelineOptions = React.useContext(PipelineDemoContext); const pipelineNodes = useDemoPipelineNodes( - showContextMenu, - showBadges, - showIcons, - badgeTooltips, - controller.getGraph().getLayout(), - showGroups + pipelineOptions.showContextMenus, + pipelineOptions.showBadges, + pipelineOptions.showIcons, + 'PipelineDagreLayout', + pipelineOptions.showGroups ); React.useEffect(() => { const spacerNodes = getSpacerNodes(pipelineNodes); const nodes = [...pipelineNodes, ...spacerNodes]; - const edgeType = showGroups ? GROUPED_EDGE_TYPE : DEFAULT_EDGE_TYPE; + const edgeType = pipelineOptions.showGroups ? GROUPED_EDGE_TYPE : DEFAULT_EDGE_TYPE; const edges = getEdgesFromNodes( nodes.filter(n => !n.group), DEFAULT_SPACER_NODE_TYPE, @@ -70,7 +69,7 @@ const TopologyPipelineLayout: React.FC = () => { type: 'graph', x: 25, y: 25, - layout: `${showGroups ? GROUP_PREFIX : ''}${PIPELINE_LAYOUT}${verticalLayout ? VERTICAL_SUFFIX : ''}` + layout: `${pipelineOptions.showGroups ? GROUP_PREFIX : ''}${PIPELINE_LAYOUT}${pipelineOptions.verticalLayout ? VERTICAL_SUFFIX : ''}` }, nodes, edges @@ -78,18 +77,18 @@ const TopologyPipelineLayout: React.FC = () => { true ); controller.getGraph().layout(); - }, [controller, pipelineNodes, showGroups, verticalLayout]); + }, [controller, pipelineNodes, pipelineOptions.showGroups, pipelineOptions.verticalLayout]); useEventListener(SELECTION_EVENT, ids => { setSelectedIds(ids); }); return ( - + }> ); -}; +}); TopologyPipelineLayout.displayName = 'TopologyPipelineLayout'; @@ -125,7 +124,9 @@ export const PipelineLayout = React.memo(() => { return ( - + + + ); }); diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineOptionsBar.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineOptionsBar.tsx new file mode 100644 index 00000000..7fddc6bf --- /dev/null +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineOptionsBar.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { Checkbox, ToolbarItem } from '@patternfly/react-core'; +import { observer } from '@patternfly/react-topology'; +import { PipelineDemoContext } from './PipelineDemoContext'; + +const PipelineOptionsBar: React.FC<{ isLayout?: boolean }> = observer(({ isLayout = false }) => { + const pipelineOptions = React.useContext(PipelineDemoContext); + + return ( + <> + + pipelineOptions.setShowIcons(checked)} + label="Show icons" + /> + + + pipelineOptions.setShowBadges(checked)} + label="Show badges" + /> + + + pipelineOptions.setShowBadgeTooltips(checked)} + label="Badge tooltips" + /> + + + pipelineOptions.setShowContextMenus(checked)} + label="Context menus" + /> + + {isLayout ? ( + <> + + pipelineOptions.setShowGroups(checked)} + label="Show groups" + /> + + + pipelineOptions.setVerticalLayout(checked)} + label="Vertical layout" + /> + + + ) : null} + + ); +}); + +export default PipelineOptionsBar; diff --git a/packages/demo-app-ts/src/demos/PipelineTasks.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineTasks.tsx similarity index 61% rename from packages/demo-app-ts/src/demos/PipelineTasks.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/PipelineTasks.tsx index d1a0246c..24bbe36a 100644 --- a/packages/demo-app-ts/src/demos/PipelineTasks.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineTasks.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { + observer, TopologyView, Visualization, VisualizationProvider, @@ -9,18 +10,23 @@ import { SELECTION_EVENT, useVisualizationController } from '@patternfly/react-topology'; -import pipelineComponentFactory from '../components/pipelineComponentFactory'; -import { usePipelineOptions } from '../utils/usePipelineOptions'; -import { useDemoPipelineNodes } from '../utils/useDemoPipelineNodes'; +import pipelineComponentFactory from './pipelineComponentFactory'; +import { useDemoPipelineNodes } from './useDemoPipelineNodes'; +import { PipelineDemoContext, PipelineDemoModel } from './PipelineDemoContext'; +import PipelineOptionsBar from './PipelineOptionsBar'; export const TASKS_TITLE = 'Tasks'; -export const PipelineTasks: React.FC = () => { +export const PipelineTasks: React.FC = observer(() => { const [selectedIds, setSelectedIds] = React.useState(); const controller = useVisualizationController(); - const { contextToolbar, showContextMenu, showBadges, showIcons, badgeTooltips } = usePipelineOptions(); - const pipelineNodes = useDemoPipelineNodes(showContextMenu, showBadges, showIcons, badgeTooltips); + const pipelineOptions = React.useContext(PipelineDemoContext); + const pipelineNodes = useDemoPipelineNodes( + pipelineOptions.showContextMenus, + pipelineOptions.showBadges, + pipelineOptions.showIcons, + ); React.useEffect(() => { controller.fromModel( @@ -42,11 +48,11 @@ export const PipelineTasks: React.FC = () => { }); return ( - + }> ); -}; +}); PipelineTasks.displayName = 'PipelineTasks'; @@ -55,7 +61,9 @@ export const TopologyPipelineTasks = React.memo(() => { controller.registerComponentFactory(pipelineComponentFactory); return ( - + + + ); }); diff --git a/packages/demo-app-ts/src/demos/TopologyPipelineDemo.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/TopologyPipelineDemo.tsx similarity index 100% rename from packages/demo-app-ts/src/demos/TopologyPipelineDemo.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/TopologyPipelineDemo.tsx diff --git a/packages/demo-app-ts/src/components/pipelineComponentFactory.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx similarity index 100% rename from packages/demo-app-ts/src/components/pipelineComponentFactory.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx diff --git a/packages/demo-app-ts/src/utils/useDemoPipelineNodes.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/useDemoPipelineNodes.tsx similarity index 87% rename from packages/demo-app-ts/src/utils/useDemoPipelineNodes.tsx rename to packages/demo-app-ts/src/demos/pipelinesDemo/useDemoPipelineNodes.tsx index 17eecc3a..ef18f00a 100644 --- a/packages/demo-app-ts/src/utils/useDemoPipelineNodes.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/useDemoPipelineNodes.tsx @@ -8,9 +8,6 @@ import { RunStatus, WhenStatus, } from '@patternfly/react-topology'; -import { CubeIcon } from '@patternfly/react-icons/dist/esm/icons/cube-icon'; -import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; -import { logos } from './logos'; export const NODE_PADDING_VERTICAL = 45; export const NODE_PADDING_HORIZONTAL = 15; @@ -45,7 +42,6 @@ export const useDemoPipelineNodes = ( showContextMenu: boolean, showBadges: boolean, showIcons: boolean, - badgeTooltips: boolean, layout?: string, showGroups = false ): PipelineNodeModel[] => @@ -68,11 +64,9 @@ export const useDemoPipelineNodes = ( // put options in data, our DEMO task node will pass them along to the TaskNode task.data = { status, - badge: showBadges ? '3/4' : undefined, - badgeTooltips, - taskIconClass: showIcons ? logos.get('icon-java') : undefined, - taskIconTooltip: showIcons ? 'Environment' : undefined, - showContextMenu, + taskProgress: '3/4', + taskType: 'java', + taskTopic: 'Environment', columnGroup: index % STATUS_PER_ROW }; @@ -94,8 +88,6 @@ export const useDemoPipelineNodes = ( ); whenTasks.forEach((task, index) => { task.data.whenStatus = index % 2 === 0 ? WhenStatus.Met : WhenStatus.Unmet; - task.data.whenOffset = DEFAULT_WHEN_OFFSET; - task.data.whenSize = DEFAULT_WHEN_SIZE; }); // Connect the tasks in each row by setting the `runAfterTasks` value for each task @@ -225,13 +217,12 @@ export const useDemoPipelineNodes = ( // put options in data, our DEMO task node will pass them along to the TaskNode iconTask1.data = { status: RunStatus.Failed, - badge: showBadges ? '3/4' : undefined, - badgeTooltips, - taskIconClass: showIcons ? logos.get('icon-java') : undefined, - taskIconTooltip: showIcons ? 'Environment' : undefined, - showContextMenu, + + taskProgress: '3/4', + taskType: 'java', + taskTopic: 'Environment', columnGroup: TASK_STATUSES.length % STATUS_PER_ROW + 1, - leadIcon: , + taskJobType: 'cubes', }; if (!layout) { @@ -256,14 +247,11 @@ export const useDemoPipelineNodes = ( // put options in data, our DEMO task node will pass them along to the TaskNode iconTask2.data = { - badge: showBadges ? '3/4' : undefined, - badgeTooltips, - taskIconClass: showIcons ? logos.get('icon-java') : undefined, - taskIconTooltip: showIcons ? 'Environment' : undefined, - showContextMenu, + taskProgress: '3/4', + taskType: 'java', + taskTopic: 'Environment', columnGroup: TASK_STATUSES.length % STATUS_PER_ROW + 1, - showStatusState: true, - leadIcon: , + taskJobType: 'link', }; if (!layout) { @@ -275,4 +263,4 @@ export const useDemoPipelineNodes = ( tasks.push(iconTask2); return [...tasks, ...finallyNodes, finallyGroup]; - }, [badgeTooltips, layout, showBadges, showContextMenu, showGroups, showIcons]); + }, [layout, showBadges, showContextMenu, showGroups, showIcons]); diff --git a/packages/demo-app-ts/src/components/FailedEdge.tsx b/packages/demo-app-ts/src/demos/statusConnectorsDemo/FailedEdge.tsx similarity index 100% rename from packages/demo-app-ts/src/components/FailedEdge.tsx rename to packages/demo-app-ts/src/demos/statusConnectorsDemo/FailedEdge.tsx diff --git a/packages/demo-app-ts/src/components/StatusConnectorNode.tsx b/packages/demo-app-ts/src/demos/statusConnectorsDemo/StatusConnectorNode.tsx similarity index 100% rename from packages/demo-app-ts/src/components/StatusConnectorNode.tsx rename to packages/demo-app-ts/src/demos/statusConnectorsDemo/StatusConnectorNode.tsx diff --git a/packages/demo-app-ts/src/demos/StatusConnectors.tsx b/packages/demo-app-ts/src/demos/statusConnectorsDemo/StatusConnectors.tsx similarity index 81% rename from packages/demo-app-ts/src/demos/StatusConnectors.tsx rename to packages/demo-app-ts/src/demos/statusConnectorsDemo/StatusConnectors.tsx index 2a3975b8..b146f53c 100644 --- a/packages/demo-app-ts/src/demos/StatusConnectors.tsx +++ b/packages/demo-app-ts/src/demos/statusConnectorsDemo/StatusConnectors.tsx @@ -10,6 +10,7 @@ import { LayoutFactory, LEFT_TO_RIGHT, NODE_SEPARATION_HORIZONTAL, + NodeModel, NodeShape, SELECTION_EVENT, TopologyControlBar, @@ -19,13 +20,11 @@ import { VisualizationProvider, VisualizationSurface } from '@patternfly/react-topology'; -import statusConnectorsComponentFactory from '../components/statusConnectorsComponentFactory'; -import { - createNode, -} from '../utils/styleUtils'; -import defaultComponentFactory from '../components/defaultComponentFactory'; +import defaultComponentFactory from '../../components/defaultComponentFactory'; +import statusConnectorsComponentFactory from './statusConnectorsComponentFactory'; const DEFAULT_CHAR_WIDTH = 8; +const DEFAULT_NODE_SIZE = 75; const getTextWidth = (text: string, font: string = '1rem RedHatText'): number => { if (!text || text.length === 0) { @@ -64,42 +63,62 @@ export const StatusConnectorsDemo: React.FunctionComponent= () => { const [selectedIds, setSelectedIds] = React.useState([]); React.useEffect(() => { - const nodes = [ - createNode({ + const nodes: NodeModel[] = [ + { id: '1', + type: 'node', shape: NodeShape.rect, + width: DEFAULT_NODE_SIZE, + height: DEFAULT_NODE_SIZE, label: 'Demo Job Template', - secondaryLabel: 'Job template', - setLocation: false, - }), - createNode({ + data: { + secondaryLabel: 'Job template', + }, + }, + { id: '2', + type: 'node', shape: NodeShape.rect, + width: DEFAULT_NODE_SIZE, + height: DEFAULT_NODE_SIZE, label: 'Demo Job Template @ 05:02:15:215PM', - secondaryLabel: 'Job template', - setLocation: false, - }), - createNode({ + data: { + secondaryLabel: 'Job template', + } + }, + { id: '3', + type: 'node', shape: NodeShape.rect, + width: DEFAULT_NODE_SIZE, + height: DEFAULT_NODE_SIZE, label: 'Approval', - secondaryLabel: 'Approval', - setLocation: false, - }), - createNode({ + data: { + secondaryLabel: 'Approval', + } + }, + { id: '4', + type: 'node', shape: NodeShape.rect, + width: DEFAULT_NODE_SIZE, + height: DEFAULT_NODE_SIZE, label: 'Demo Project', - secondaryLabel: 'Project', - setLocation: false, - }), - createNode({ + data: { + secondaryLabel: 'Project', + } + }, + { id: '5', + type: 'node', shape: NodeShape.rect, + width: DEFAULT_NODE_SIZE, + height: DEFAULT_NODE_SIZE, label: 'Cleanup Activity Stream', - secondaryLabel: 'System job', - setLocation: false, - }), + data: { + secondaryLabel: 'System job', + } + }, ]; const edges: EdgeModel[] = [ @@ -161,7 +180,6 @@ export const StatusConnectorsDemo: React.FunctionComponent= () => { nodes.forEach((node) => { node.width = getTextWidth(node.label); - node.height = 75; }); nodes[1].width = Math.max(nodes[1].width, nodes[2].width); nodes[2].width = nodes[1].width; diff --git a/packages/demo-app-ts/src/components/SuccessEdge.tsx b/packages/demo-app-ts/src/demos/statusConnectorsDemo/SuccessEdge.tsx similarity index 100% rename from packages/demo-app-ts/src/components/SuccessEdge.tsx rename to packages/demo-app-ts/src/demos/statusConnectorsDemo/SuccessEdge.tsx diff --git a/packages/demo-app-ts/src/components/statusConnectorsComponentFactory.ts b/packages/demo-app-ts/src/demos/statusConnectorsDemo/statusConnectorsComponentFactory.ts similarity index 93% rename from packages/demo-app-ts/src/components/statusConnectorsComponentFactory.ts rename to packages/demo-app-ts/src/demos/statusConnectorsDemo/statusConnectorsComponentFactory.ts index d30552ba..7a0f3ed1 100644 --- a/packages/demo-app-ts/src/components/statusConnectorsComponentFactory.ts +++ b/packages/demo-app-ts/src/demos/statusConnectorsDemo/statusConnectorsComponentFactory.ts @@ -6,7 +6,7 @@ import { GraphComponent, withPanZoom } from '@patternfly/react-topology'; -import DefaultEdge from './DefaultEdge'; +import DefaultEdge from '../../components/DemoDefaultEdge'; import StatusConnectorNode from './StatusConnectorNode'; import SuccessEdge from './SuccessEdge'; import FailedEdge from './FailedEdge'; diff --git a/packages/demo-app-ts/src/components/useSourceStatusAnchor.tsx b/packages/demo-app-ts/src/demos/statusConnectorsDemo/useSourceStatusAnchor.tsx similarity index 100% rename from packages/demo-app-ts/src/components/useSourceStatusAnchor.tsx rename to packages/demo-app-ts/src/demos/statusConnectorsDemo/useSourceStatusAnchor.tsx diff --git a/packages/demo-app-ts/src/components/useTargetStatusAnchor.tsx b/packages/demo-app-ts/src/demos/statusConnectorsDemo/useTargetStatusAnchor.tsx similarity index 100% rename from packages/demo-app-ts/src/components/useTargetStatusAnchor.tsx rename to packages/demo-app-ts/src/demos/statusConnectorsDemo/useTargetStatusAnchor.tsx diff --git a/packages/demo-app-ts/src/components/StyleEdge.tsx b/packages/demo-app-ts/src/demos/stylesDemo/StyleEdge.tsx similarity index 100% rename from packages/demo-app-ts/src/components/StyleEdge.tsx rename to packages/demo-app-ts/src/demos/stylesDemo/StyleEdge.tsx diff --git a/packages/demo-app-ts/src/components/StyleGroup.tsx b/packages/demo-app-ts/src/demos/stylesDemo/StyleGroup.tsx similarity index 100% rename from packages/demo-app-ts/src/components/StyleGroup.tsx rename to packages/demo-app-ts/src/demos/stylesDemo/StyleGroup.tsx diff --git a/packages/demo-app-ts/src/components/StyleNode.tsx b/packages/demo-app-ts/src/demos/stylesDemo/StyleNode.tsx similarity index 100% rename from packages/demo-app-ts/src/components/StyleNode.tsx rename to packages/demo-app-ts/src/demos/stylesDemo/StyleNode.tsx diff --git a/packages/demo-app-ts/src/demos/Styles.tsx b/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx similarity index 98% rename from packages/demo-app-ts/src/demos/Styles.tsx rename to packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx index af652065..e49c6f94 100644 --- a/packages/demo-app-ts/src/demos/Styles.tsx +++ b/packages/demo-app-ts/src/demos/stylesDemo/Styles.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; import { BadgeLocation, EdgeModel, @@ -12,10 +13,10 @@ import { useComponentFactory, useModel } from '@patternfly/react-topology'; -import withTopologySetup from '../utils/withTopologySetup'; -import defaultComponentFactory from '../components/defaultComponentFactory'; -import stylesComponentFactory from '../components/stylesComponentFactory'; -import { logos } from '../utils/logos'; +import withTopologySetup from '../../utils/withTopologySetup'; +import defaultComponentFactory from '../../components/defaultComponentFactory'; +import stylesComponentFactory from './stylesComponentFactory'; +import { logos } from '../../utils/logos'; import { AlternateTerminalTypes, createBadgeNodes, @@ -32,9 +33,8 @@ import { EDGE_TERMINAL_TYPES_COUNT, RIGHT_LABEL_COLUMN_WIDTH, STATUS_VALUES -} from '../utils/styleUtils'; -import { DataTypes } from '../components/StyleNode'; -import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +} from './styleUtils'; +import { DataTypes } from './StyleNode'; export const NodeStyles = withTopologySetup(() => { useComponentFactory(defaultComponentFactory); diff --git a/packages/demo-app-ts/src/utils/styleUtils.ts b/packages/demo-app-ts/src/demos/stylesDemo/styleUtils.ts similarity index 92% rename from packages/demo-app-ts/src/utils/styleUtils.ts rename to packages/demo-app-ts/src/demos/stylesDemo/styleUtils.ts index cc0b33ea..c71a971c 100644 --- a/packages/demo-app-ts/src/utils/styleUtils.ts +++ b/packages/demo-app-ts/src/demos/stylesDemo/styleUtils.ts @@ -2,7 +2,6 @@ import * as React from 'react'; import { BadgeLocation, EdgeAnimationSpeed, - EdgeModel, EdgeStyle, EdgeTerminalType, LabelPosition, @@ -10,8 +9,8 @@ import { NodeShape, NodeStatus } from '@patternfly/react-topology'; -import { DataTypes } from '../components/StyleNode'; -import { logos } from './logos'; +import { DataTypes } from './StyleNode'; +import { logos } from '../../utils/logos'; export const ROW_HEIGHT = 140; export const BOTTOM_LABEL_ROW_HEIGHT = 165; @@ -20,13 +19,6 @@ export const RIGHT_LABEL_COLUMN_WIDTH = 200; export const DEFAULT_NODE_SIZE = 75; -export const NODE_STATUSES = [ - NodeStatus.danger, - NodeStatus.success, - NodeStatus.warning, - NodeStatus.info, - NodeStatus.default -]; export const NODE_SHAPES = [ NodeShape.ellipse, NodeShape.rect, @@ -152,33 +144,6 @@ export const createNode = (options: { return nodeModel; }; -export const createEdge = ( - sourceId: string, - targetId: string, - options: { - style?: EdgeStyle; - animation?: EdgeAnimationSpeed; - terminalType?: EdgeTerminalType; - terminalStatus?: NodeStatus; - tag?: string; - tagStatus?: string; - } -): EdgeModel => ({ - id: `edge-${sourceId}-${targetId}`, - type: 'edge', - source: sourceId, - 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, - tag: options.tag, - tagStatus: options.tagStatus - } -}); - const createStatusNodes = ( shape: NodeShape, column: number, diff --git a/packages/demo-app-ts/src/components/stylesComponentFactory.tsx b/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx similarity index 97% rename from packages/demo-app-ts/src/components/stylesComponentFactory.tsx rename to packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx index abc1ef7d..b747aa51 100644 --- a/packages/demo-app-ts/src/components/stylesComponentFactory.tsx +++ b/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx @@ -29,8 +29,8 @@ import { import StyleNode from './StyleNode'; import StyleGroup from './StyleGroup'; import StyleEdge from './StyleEdge'; -import CustomPathNode from './CustomPathNode'; -import CustomPolygonNode from './CustomPolygonNode'; +import CustomPathNode from '../../components/CustomPathNode'; +import CustomPolygonNode from '../../components/CustomPolygonNode'; const CONNECTOR_SOURCE_DROP = 'connector-src-drop'; const CONNECTOR_TARGET_DROP = 'connector-target-drop'; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx new file mode 100644 index 00000000..652b0ff9 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoContext.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import { action, makeObservable, observable } from 'mobx'; +import { LabelPosition } from '@patternfly/react-topology'; +import { GeneratorEdgeOptions, GeneratorNodeOptions } from './generator'; + +export class DemoModel { + protected nodeOptionsP: GeneratorNodeOptions = { + showStatus: false, + showShapes: false, + showDecorators: false, + labels: true, + secondaryLabels: false, + labelPosition: LabelPosition.bottom, + badges: false, + icons: false, + contextMenus: false, + hulledOutline: true, + }; + protected edgeOptionsP: GeneratorEdgeOptions = { + showStyles: false, + showStatus: false, + showAnimations: false, + showTags: false, + terminalTypes: false, + }; + protected nestedLevelP: number = 0; + protected creationCountsP: { numNodes: number; numEdges: number; numGroups: number } = { + numNodes: 6, + numEdges: 2, + numGroups: 1, + }; + protected layoutP: string = 'ColaNoForce'; + protected medScaleP: number = 0.5; + protected lowScaleP: number = 0.3; + + constructor() { + makeObservable< + DemoModel, + | 'nodeOptionsP' + | 'edgeOptionsP' + | 'nestedLevelP' + | 'creationCountsP' + | 'layoutP' + | 'medScaleP' + | 'lowScaleP' + | 'setNodeOptions' + | 'setEdgeOptions' + | 'setNestedLevel' + | 'setCreationCounts' + | 'setLayout' + | 'setMedScale' + | 'setLowScale' + >(this, { + nodeOptionsP: observable.ref, + edgeOptionsP: observable.shallow, + nestedLevelP: observable, + creationCountsP: observable.shallow, + layoutP: observable, + medScaleP: observable, + lowScaleP: observable, + setNodeOptions: action, + setEdgeOptions: action, + setNestedLevel: action, + setCreationCounts: action, + setLayout: action, + setMedScale: action, + setLowScale: action, + }); + } + + public get nodeOptions(): GeneratorNodeOptions { + return this.nodeOptionsP; + } + public setNodeOptions = (options: GeneratorNodeOptions): void => { + this.nodeOptionsP = options; + } + + public get edgeOptions(): GeneratorEdgeOptions { + return this.edgeOptionsP; + } + public setEdgeOptions = (options: GeneratorEdgeOptions): void => { + this.edgeOptionsP = options; + } + + public get nestedLevel(): number { + return this.nestedLevelP; + } + public setNestedLevel = (show: number): void => { + this.nestedLevelP = show; + } + + public get creationCounts(): { numNodes: number; numEdges: number; numGroups: number } { + return this.creationCountsP; + } + public setCreationCounts = (counts: { numNodes: number; numEdges: number; numGroups: number }): void => { + this.creationCountsP = counts; + } + + public get layout(): string { + return this.layoutP; + } + public setLayout = (newLayout: string): void => { + this.layoutP = newLayout; + } + + public get medScale(): number { + return this.medScaleP; + } + public setMedScale = (scale: number): void => { + this.medScaleP = scale; + } + + public get lowScale(): number { + return this.lowScaleP; + } + public setLowScale = (scale: number): void => { + this.lowScaleP = scale; + } +} + +export const DemoContext = React.createContext(new DemoModel()); \ No newline at end of file diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx new file mode 100644 index 00000000..6315fb69 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoEdge.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { + DefaultEdge, + Edge, EdgeTerminalType, + getEdgeAnimationDuration, + WithContextMenuProps, + WithSelectionProps +} from '@patternfly/react-topology'; +import { DemoContext } from './DemoContext'; +import { + EDGE_ANIMATION_SPEEDS, + EDGE_STYLES, + EDGE_TERMINAL_TYPES, + NODE_STATUSES +} from './generator'; + +type DemoEdgeProps = { + element: Edge; +} & WithContextMenuProps & + WithSelectionProps; + +const DemoEdge: React.FunctionComponent = ({ element, ...rest }) => { + const options = React.useContext(DemoContext).edgeOptions; + const data = element.getData(); + + return ( + + ); +}; + +export default observer(DemoEdge); diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoGroup.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoGroup.tsx new file mode 100644 index 00000000..577d0693 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoGroup.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { + DefaultGroup, + GraphElement, + Node, + observer, + ScaleDetailsLevel, + WithContextMenuProps, + WithDragNodeProps, + WithSelectionProps +} from '@patternfly/react-topology'; +import AlternateIcon from '@patternfly/react-icons/dist/esm/icons/regions-icon'; +import DefaultIcon from '@patternfly/react-icons/dist/esm/icons/builder-image-icon'; +import { DemoContext } from './DemoContext'; +import { DataTypes, DEFAULT_NODE_SIZE } from './generator'; + +const ICON_PADDING = 20; + +type DemoGroupProps = { + element: GraphElement; +} & WithContextMenuProps & + WithDragNodeProps & + WithSelectionProps; + +const DemoGroup: React.FunctionComponent = ({ + element, + onContextMenu, + ...rest +}) => { + const options = React.useContext(DemoContext).nodeOptions; + const groupElement = element as Node; + const data = element.getData(); + const detailsLevel = element.getGraph().getDetailsLevel(); + + const getTypeIcon = (dataType?: DataTypes): any => { + switch (dataType) { + case DataTypes.Alternate: + return AlternateIcon; + default: + return DefaultIcon; + } + }; + + const renderIcon = (): React.ReactNode => { + const iconSize = Math.min(DEFAULT_NODE_SIZE, DEFAULT_NODE_SIZE) - ICON_PADDING * 2; + const Component = getTypeIcon(data.dataType); + + return ( + + + + ); + }; + + return ( + + {groupElement.isCollapsed() ? renderIcon() : null} + + ); +}; + +export default observer(DemoGroup); diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx new file mode 100644 index 00000000..be055614 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/DemoNode.tsx @@ -0,0 +1,206 @@ +import * as React from 'react'; +import { + Decorator, + DEFAULT_DECORATOR_RADIUS, + DEFAULT_LAYER, + DefaultNode, + Ellipse, + getDefaultShapeDecoratorCenter, + GraphElement, + Hexagon, + Layer, + Node, + NodeShape, + NodeStatus, + observer, + Octagon, + Rectangle, + Rhombus, + ScaleDetailsLevel, + ShapeProps, + Stadium, + TOP_LAYER, + TopologyQuadrant, + Trapezoid, + useHover, + WithContextMenuProps, + WithCreateConnectorProps, + WithDragNodeProps, + WithSelectionProps +} from '@patternfly/react-topology'; +import DefaultIcon from '@patternfly/react-icons/dist/esm/icons/builder-image-icon'; +import AlternateIcon from '@patternfly/react-icons/dist/esm/icons/regions-icon'; +import FolderOpenIcon from '@patternfly/react-icons/dist/esm/icons/folder-open-icon'; +import BlueprintIcon from '@patternfly/react-icons/dist/esm/icons/blueprint-icon'; +import PauseCircle from '@patternfly/react-icons/dist/esm/icons/pause-circle-icon'; +import Thumbtack from '@patternfly/react-icons/dist/esm/icons/thumbtack-icon'; +import SignOutAltIcon from '@patternfly/react-icons/dist/esm/icons/skull-icon'; +import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon'; +import { DataTypes, GeneratedNodeData, GeneratorNodeOptions } from './generator'; +import { DemoContext } from './DemoContext'; +import { logos } from '../../utils/logos'; + +const ICON_PADDING = 20; + +type DemoNodeProps = { + element: GraphElement; + getCustomShape?: (node: Node) => React.FunctionComponent; + getShapeDecoratorCenter?: (quadrant: TopologyQuadrant, node: Node) => { x: number; y: number }; + showLabel?: boolean; // Defaults to true + labelIcon?: React.ComponentClass; + showStatusDecorator?: boolean; // Defaults to false + regrouping?: boolean; + dragging?: boolean; +} & WithContextMenuProps & + WithCreateConnectorProps & + WithDragNodeProps & + WithSelectionProps; + +const getTypeIcon = (dataType?: DataTypes): any => { + switch (dataType) { + case DataTypes.Alternate: + return AlternateIcon; + default: + return DefaultIcon; + } +}; + +const renderIcon = (data: { dataType?: DataTypes }, element: Node): React.ReactNode => { + const { width, height } = element.getDimensions(); + const shape = element.getNodeShape(); + const iconSize = + (shape === NodeShape.trapezoid ? width : Math.min(width, height)) - + (shape === NodeShape.stadium ? 5 : ICON_PADDING) * 2; + const Component = getTypeIcon(data.dataType); + + return ( + + + + ); +}; + +const renderDecorator = ( + element: Node, + quadrant: TopologyQuadrant, + icon: React.ReactNode, + getShapeDecoratorCenter?: ( + quadrant: TopologyQuadrant, + node: Node, + radius?: number + ) => { + x: number; + y: number; + } +): React.ReactNode => { + const { x, y } = getShapeDecoratorCenter + ? getShapeDecoratorCenter(quadrant, element) + : getDefaultShapeDecoratorCenter(quadrant, element); + + return ; +}; + +const renderDecorators = ( + options: GeneratorNodeOptions, + element: Node, + getShapeDecoratorCenter?: ( + quadrant: TopologyQuadrant, + node: Node + ) => { + x: number; + y: number; + } +): React.ReactNode => { + const data = element.getData() as GeneratedNodeData; + return ( + <> + {!options.showStatus || data.status === NodeStatus.default + ? renderDecorator(element, TopologyQuadrant.upperLeft, , getShapeDecoratorCenter) + : null} + {renderDecorator(element, TopologyQuadrant.upperRight, , getShapeDecoratorCenter)} + {renderDecorator(element, TopologyQuadrant.lowerLeft, , getShapeDecoratorCenter)} + {renderDecorator(element, TopologyQuadrant.lowerRight, , getShapeDecoratorCenter)} + + ); +}; + +const getShapeComponent = (shape: NodeShape): React.FunctionComponent => { + switch (shape) { + case NodeShape.circle: + case NodeShape.ellipse: + return Ellipse; + case NodeShape.stadium: + return Stadium; + case NodeShape.rhombus: + return Rhombus; + case NodeShape.trapezoid: + return Trapezoid; + case NodeShape.rect: + return Rectangle; + case NodeShape.hexagon: + return Hexagon; + case NodeShape.octagon: + return Octagon; + default: + return Ellipse; + } +}; + +const DemoNode: React.FunctionComponent = observer(({ + element, + onContextMenu, + dragging, + onShowCreateConnector, + onHideCreateConnector, + ...rest +}) => { + const options = React.useContext(DemoContext).nodeOptions; + const nodeElement = element as Node; + const data = element.getData() as GeneratedNodeData; + const detailsLevel = element.getGraph().getDetailsLevel(); + const [hover, hoverRef] = useHover(); + + React.useEffect(() => { + if (detailsLevel === ScaleDetailsLevel.low) { + onHideCreateConnector && onHideCreateConnector(); + } + }, [detailsLevel, onHideCreateConnector]); + + const labelIconClass = data.index % 2 === 0 && logos.get('icon-java'); + const LabelIcon = data.index % 2 === 1 ? SignOutAltIcon as any : undefined; + + return ( + + + } + labelIconClass={options.icons && labelIconClass} + secondaryLabel={options.secondaryLabels && data.subTitle} + nodeStatus={options.showStatus && data.status} + getCustomShape={options.showShapes ? () => getShapeComponent(data.shape) : undefined} + badge={options.badges ? data.objectType : undefined} + attachments={ + (hover || detailsLevel === ScaleDetailsLevel.high) && options.showDecorators && + renderDecorators(options, nodeElement, rest.getShapeDecoratorCenter) + } + > + {(hover || detailsLevel !== ScaleDetailsLevel.low) && renderIcon(data, nodeElement)} + + + + ); +}); + +export default DemoNode; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx new file mode 100644 index 00000000..52f81622 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsContextBar.tsx @@ -0,0 +1,288 @@ +import React from 'react'; +import { + Button, + Flex, + FlexItem, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + TextInput, + ToolbarItem, +} from '@patternfly/react-core'; +import { observer } from '@patternfly/react-topology'; +import { DemoContext } from './DemoContext'; + +const OptionsContextBar: React.FC = observer(() => { + const options = React.useContext(DemoContext); + const [nodeOptionsOpen, setNodeOptionsOpen] = React.useState(false); + const [edgeOptionsOpen, setEdgeOptionsOpen] = React.useState(false); + const [numNodes, setNumNodes] = React.useState(options.creationCounts.numNodes); + const [numEdges, setNumEdges] = React.useState(options.creationCounts.numEdges); + const [numGroups, setNumGroups] = React.useState(options.creationCounts.numGroups); + const [nestedLevel, setNestedLevel] = React.useState(options.nestedLevel); + + const renderNodeOptionsDropdown = () => { + const nodeOptionsToggle = (toggleRef: React.Ref) => ( + setNodeOptionsOpen((prev) => !prev)} + isExpanded={nodeOptionsOpen} + style={ + { + width: '180px' + } as React.CSSProperties + } + > + Node options + + ); + + return ( + + ); + }; + + const renderEdgeOptionsDropdown = () => { + const selectContent = ( + + options.setEdgeOptions({ ...options.edgeOptions, showStatus: !options.edgeOptions.showStatus })} + > + Status + + options.setEdgeOptions({ ...options.edgeOptions, showStyles: !options.edgeOptions.showStyles })} + > + Styles + + options.setEdgeOptions({ ...options.edgeOptions, showAnimations: !options.edgeOptions.showAnimations })} + > + Animations + + options.setEdgeOptions({ ...options.edgeOptions, terminalTypes: !options.edgeOptions.terminalTypes })} + > + Terminal type + + options.setEdgeOptions({ ...options.edgeOptions, showTags: !options.edgeOptions.showTags })} + > + Tags + + + ); + const edgeOptionsToggle = (toggleRef: React.Ref) => ( + setEdgeOptionsOpen((prev) => !prev)} + isExpanded={edgeOptionsOpen} + style={ + { + width: '180px' + } as React.CSSProperties + } + > + Edge options + + ); + + return ( + + ); + }; + + const updateValue = (value: number, min: number, max: number, setter: any): void => { + if (value >= min && value <= max) { + setter(value); + } + }; + + return ( + + + + + + 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()} + {renderEdgeOptionsDropdown()} + + + + ); +}); + +export default OptionsContextBar; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsViewBar.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsViewBar.tsx new file mode 100644 index 00000000..871178e9 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/OptionsViewBar.tsx @@ -0,0 +1,201 @@ +import React from 'react'; +import { + Button, + Dropdown, + DropdownItem, + DropdownList, + Flex, + MenuToggle, + MenuToggleElement, + Split, + SplitItem, + TextInput, + ToolbarItem, + Tooltip, +} from '@patternfly/react-core'; +import { Controller, Model, observer } from '@patternfly/react-topology'; +import { DemoContext } from './DemoContext'; + +const OptionsContextBar: React.FC<{ controller: Controller }> = observer(({ controller }) => { + const options = React.useContext(DemoContext); + const [layoutDropdownOpen, setLayoutDropdownOpen] = React.useState(false); + const [savedModel, setSavedModel] = React.useState(); + const [modelSaved, setModelSaved] = React.useState(false); + const newNodeCount = React.useRef(0); + + const updateLayout = (newLayout: string) => { + options.setLayout(newLayout); + setLayoutDropdownOpen(false); + }; + + const layoutDropdown = ( + + + + + + ) => ( + setLayoutDropdownOpen(!layoutDropdownOpen)}> + {options.layout} + + )} + isOpen={layoutDropdownOpen} + onOpenChange={(isOpen) => setLayoutDropdownOpen(isOpen)} + > + + updateLayout('Force')}> + Force + + updateLayout('Dagre')}> + Dagre + + updateLayout('Cola')}> + Cola + + updateLayout('ColaGroups')}> + ColaGroups + + updateLayout('ColaNoForce')}> + ColaNoForce + + updateLayout('Grid')}> + Grid + + updateLayout('Concentric')}> + Concentric + + updateLayout('BreadthFirst')}> + BreadthFirst + + + + + + ); + + const saveModel = () => { + setSavedModel(controller.toModel()); + setModelSaved(true); + setTimeout(() => { + setModelSaved(false); + }, 2000); + }; + + const restoreLayout = () => { + if (savedModel) { + const currentModel = controller.toModel(); + currentModel.graph = { + ...currentModel.graph, + x: savedModel.graph.x, + y: savedModel.graph.y, + visible: savedModel.graph.visible, + style: savedModel.graph.style, + layout: savedModel.graph.layout, + scale: savedModel.graph.scale, + scaleExtent: savedModel.graph.scaleExtent, + layers: savedModel.graph.layers, + }; + currentModel.nodes = currentModel.nodes.map((n) => { + const savedNode = savedModel.nodes.find((sn) => sn.id === n.id); + if (!savedNode) { + return n; + } + return { + ...n, + x: savedNode.x, + y: savedNode.y, + visible: savedNode.visible, + style: savedNode.style, + collapsed: savedNode.collapsed, + width: savedNode.width, + height: savedNode.height, + shape: savedNode.shape, + }; + }); + controller.fromModel(currentModel, false); + + if (savedModel.graph.layout !== options.layout) { + options.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); + }; + + return ( + + + {layoutDropdown} + + + + + + + + + + + + + + + + Medium Scale: + { + const newValue = parseFloat(val); + if (!Number.isNaN(newValue) && newValue > options.lowScale && newValue >= 0.01 && newValue <= 1.0) { + options.setMedScale(parseFloat(val)); + } + }} + /> + + + Low Scale: + { + const newValue = parseFloat(val); + if (!Number.isNaN(newValue) && newValue < options.medScale && newValue >= 0.01 && newValue <= 1.0) { + options.setLowScale(parseFloat(val)); + } + }} + /> + + + + + ); +}); + +export default OptionsContextBar; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/TopologyPackage.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/TopologyPackage.tsx new file mode 100644 index 00000000..e5c223f1 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/TopologyPackage.tsx @@ -0,0 +1,162 @@ +import * as React from 'react'; +import { action } from 'mobx'; +import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +import { + createTopologyControlButtons, + defaultControlButtonsOptions, + GRAPH_POSITION_CHANGE_EVENT, + GRAPH_LAYOUT_END_EVENT, + SELECTION_EVENT, + SelectionEventListener, + TopologyControlBar, + TopologySideBar, + TopologyView, + useEventListener, + useVisualizationController, + Visualization, + VisualizationProvider, + VisualizationSurface, observer +} from '@patternfly/react-topology'; +import defaultLayoutFactory from '../../layouts/defaultLayoutFactory'; +import defaultComponentFactory from '../../components/defaultComponentFactory'; +import { generateDataModel } from './generator'; +import OptionsContextBar from './OptionsContextBar'; +import OptionsViewBar from './OptionsViewBar'; +import { DemoContext } from './DemoContext'; +import demoComponentFactory from './demoComponentFactory'; +import { graphPositionChangeListener, layoutEndListener } from './listeners'; + +interface TopologyViewComponentProps { + useSidebar: boolean; + sideBarResizable?: boolean; +} + +const TopologyViewComponent: React.FunctionComponent = observer(({ + useSidebar, + sideBarResizable = false +}) => { + const [selectedIds, setSelectedIds] = React.useState([]); + const controller = useVisualizationController(); + const options = React.useContext(DemoContext); + + React.useEffect(() => { + const dataModel = generateDataModel( + options.creationCounts.numNodes, + options.creationCounts.numGroups, + options.creationCounts.numEdges, + options.nestedLevel, + ); + + const model = { + graph: { + id: 'g1', + type: 'graph', + layout: options.layout, + }, + ...dataModel + }; + + controller.fromModel(model, true); + }, [controller, options.creationCounts, options.layout, options.nestedLevel]); + + useEventListener(SELECTION_EVENT, ids => { + setSelectedIds(ids); + }); + + React.useEffect(() => { + + controller.addEventListener(GRAPH_POSITION_CHANGE_EVENT, graphPositionChangeListener); + controller.addEventListener(GRAPH_LAYOUT_END_EVENT, layoutEndListener); + + return () => { + controller.removeEventListener(GRAPH_POSITION_CHANGE_EVENT, graphPositionChangeListener); + controller.removeEventListener(GRAPH_LAYOUT_END_EVENT, layoutEndListener); + }; + }, [controller]); + + React.useEffect(() => { + controller.getGraph().setDetailsLevelThresholds({ + low: options.lowScale, + medium: options.medScale + }); + }, [controller, options.lowScale, options.medScale]); + + const topologySideBar = ( + setSelectedIds([])}> +
{selectedIds?.[0]}
+
+ ); + + return ( + { + controller.getGraph().scaleBy(4 / 3); + }), + zoomOutCallback: action(() => { + controller.getGraph().scaleBy(0.75); + }), + fitToScreenCallback: action(() => { + controller.getGraph().fit(80); + }), + resetViewCallback: action(() => { + controller.getGraph().reset(); + controller.getGraph().layout(); + }), + legend: false + })} + /> + } + contextToolbar={} + viewToolbar={ } + sideBar={useSidebar && topologySideBar} + sideBarOpen={useSidebar && !!selectedIds?.length} + sideBarResizable={sideBarResizable} + > + + + ); +}); + +export const Topology: React.FC<{ useSidebar?: boolean, sideBarResizable?: boolean }> = ({ + useSidebar = false, + sideBarResizable = false +}) => { + const controller = new Visualization(); + controller.registerLayoutFactory(defaultLayoutFactory); + controller.registerComponentFactory(defaultComponentFactory); + controller.registerComponentFactory(demoComponentFactory); + + return ( + + + + ); +}; + +export const TopologyPackage: React.FunctionComponent = () => { + const [activeKey, setActiveKey] = React.useState(0); + + const handleTabClick = (_event: React.MouseEvent, tabIndex: string | number) => { + setActiveKey(tabIndex); + }; + + return ( +
+ + Topology}> + + + With Side Bar}> + + + With Resizeable Side Bar}> + + + +
+ ); +}; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/demoComponentFactory.tsx b/packages/demo-app-ts/src/demos/topologyPackageDemo/demoComponentFactory.tsx new file mode 100644 index 00000000..927264e8 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/demoComponentFactory.tsx @@ -0,0 +1,143 @@ +import * as React from 'react'; +import { + GraphElement, + ComponentFactory, + withContextMenu, + ContextMenuSeparator, + ContextMenuItem, + withDragNode, + withSelection, + ModelKind, + DragObjectWithType, + Node, + withPanZoom, + GraphComponent, + withCreateConnector, + Graph, + isNode, + withDndDrop, + Edge, + withTargetDrag, + withSourceDrag, + nodeDragSourceSpec, + nodeDropTargetSpec, + groupDropTargetSpec, + graphDropTargetSpec, + NODE_DRAG_TYPE, + CREATE_CONNECTOR_DROP_TYPE +} from '@patternfly/react-topology'; +import CustomPathNode from '../../components/CustomPathNode'; +import CustomPolygonNode from '../../components/CustomPolygonNode'; +import DemoGroup from './DemoGroup'; +import DemoNode from './DemoNode'; +import DemoEdge from './DemoEdge'; + +const CONNECTOR_SOURCE_DROP = 'connector-src-drop'; +const CONNECTOR_TARGET_DROP = 'connector-target-drop'; + +interface EdgeProps { + element: Edge; +} + +const contextMenuItem = (label: string, i: number): React.ReactElement => { + if (label === '-') { + return ; + } + return ( + // eslint-disable-next-line no-alert + alert(`Selected: ${label}`)}> + {label} + + ); +}; + +const createContextMenuItems = (...labels: string[]): React.ReactElement[] => labels.map(contextMenuItem); + +const defaultMenu = createContextMenuItems('First', 'Second', 'Third', '-', 'Fourth'); + +const demoComponentFactory: ComponentFactory = ( + kind: ModelKind, + type: string +): React.ComponentType<{ element: GraphElement }> | undefined => { + if (kind === ModelKind.graph) { + return withDndDrop(graphDropTargetSpec([NODE_DRAG_TYPE]))(withPanZoom()(GraphComponent)); + } + switch (type) { + case 'node': + return withCreateConnector((source: Node, target: Node | Graph): void => { + let targetId; + const model = source.getController().toModel(); + if (isNode(target)) { + targetId = target.getId(); + } else { + return; + } + const id = `e${source.getGraph().getEdges().length + 1}`; + if (!model.edges) { + model.edges = []; + } + model.edges.push({ + id, + type: 'edge', + source: source.getId(), + target: targetId + }); + source.getController().fromModel(model); + })( + withDndDrop(nodeDropTargetSpec([CONNECTOR_SOURCE_DROP, CONNECTOR_TARGET_DROP, CREATE_CONNECTOR_DROP_TYPE]))( + withContextMenu(() => defaultMenu)( + withDragNode(nodeDragSourceSpec('node', true, true))(withSelection()(DemoNode)) + ) + ) + ); + case 'node-path': + return CustomPathNode; + case 'node-polygon': + return CustomPolygonNode; + case 'group': + return withDndDrop(groupDropTargetSpec)( + withContextMenu(() => defaultMenu)(withDragNode(nodeDragSourceSpec('group'))(withSelection()(DemoGroup))) + ); + case 'edge': + return withSourceDrag({ + item: { type: CONNECTOR_SOURCE_DROP }, + begin: (monitor, props) => { + props.element.raise(); + return props.element; + }, + drag: (event, monitor, props) => { + props.element.setStartPoint(event.x, event.y); + }, + end: (dropResult, monitor, props) => { + if (monitor.didDrop() && dropResult && props) { + props.element.setSource(dropResult); + } + props.element.setStartPoint(); + } + })( + withTargetDrag({ + item: { type: CONNECTOR_TARGET_DROP }, + begin: (monitor, props) => { + props.element.raise(); + return props.element; + }, + drag: (event, monitor, props) => { + props.element.setEndPoint(event.x, event.y); + }, + end: (dropResult, monitor, props) => { + if (monitor.didDrop() && dropResult && props) { + props.element.setTarget(dropResult); + } + props.element.setEndPoint(); + }, + collect: monitor => ({ + dragging: monitor.isDragging() + }) + })(withContextMenu(() => defaultMenu)(withSelection()(DemoEdge))) + ); + default: + return undefined; + } +}; + +export default demoComponentFactory; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts b/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts new file mode 100644 index 00000000..5e769b80 --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/generator.ts @@ -0,0 +1,192 @@ +import { + EdgeAnimationSpeed, + EdgeModel, + EdgeStyle, + EdgeTerminalType, + LabelPosition, + Model, + NodeModel, + NodeShape, + NodeStatus +} from '@patternfly/react-topology'; + +export const DEFAULT_NODE_SIZE = 75; + +export const NODE_STATUSES = [ + NodeStatus.danger, + NodeStatus.success, + NodeStatus.warning, + NodeStatus.info, + NodeStatus.default +]; +export const NODE_SHAPES = [ + NodeShape.ellipse, + NodeShape.rect, + NodeShape.rhombus, + NodeShape.trapezoid, + NodeShape.hexagon, + NodeShape.octagon, + NodeShape.stadium +]; + +export const EDGE_STYLES = [ + EdgeStyle.dashed, + EdgeStyle.dashedMd, + EdgeStyle.dotted, + EdgeStyle.dashedLg, + EdgeStyle.dashedXl, + EdgeStyle.solid +]; + +export const EDGE_ANIMATION_SPEEDS = [ + EdgeAnimationSpeed.medium, + EdgeAnimationSpeed.mediumFast, + EdgeAnimationSpeed.mediumSlow, + EdgeAnimationSpeed.fast, + EdgeAnimationSpeed.none, + EdgeAnimationSpeed.slow +]; + +export const EDGE_TERMINAL_TYPES = [ + EdgeTerminalType.directionalAlt, + EdgeTerminalType.circle, + EdgeTerminalType.square, + EdgeTerminalType.cross, + EdgeTerminalType.directional, + EdgeTerminalType.none, +]; + +const getRandomNode = (numNodes: number, notNode = -1): number => { + let node = Math.floor(Math.random() * numNodes); + if (node === notNode) { + node = getRandomNode(numNodes, notNode); + } + return node; +}; + +export enum DataTypes { + Default, + Alternate +} + +export interface GeneratedNodeData { + index?: number; + dataType?: DataTypes; + subTitle?: string; + objectType?: string; + shape?: NodeShape; + status?: NodeStatus; +} + +export interface GeneratorNodeOptions { + showStatus?: boolean; + showShapes?: boolean; + showDecorators?: boolean; + labels?: boolean; + secondaryLabels?: boolean; + labelPosition: LabelPosition; + badges?: boolean; + icons?: boolean; + contextMenus?: boolean; + hulledOutline?: boolean; +} + +export interface GeneratorEdgeOptions { + showStyles?: boolean; + showStatus?: boolean; + showAnimations?: boolean; + showTags?: boolean; + terminalTypes?: boolean; +} + +const createNode = (index: number): NodeModel => ({ + id: `node-${index}`, + label: `Node ${index} Title`, + type: 'node', + width: DEFAULT_NODE_SIZE, + height: DEFAULT_NODE_SIZE, + data: { + dataType: 'Default', + index, + subTitle: `Node subtitle`, + objectType: 'CS', + shape: NODE_SHAPES[Math.round(Math.random() * (NODE_SHAPES.length - 1))], + status: NODE_STATUSES[index % NODE_STATUSES.length], + } +}); + +export const generateDataModel = ( + numNodes: number, + numGroups: number, + numEdges: number, + groupDepth: number = 0, +): Model => { + const groups: NodeModel[] = []; + const nodes: NodeModel[] = []; + const edges: EdgeModel[] = []; + + const createGroup = ( + childNodes: NodeModel[], + baseId: string = 'Group', + index: number, + level: number = 0 + ): NodeModel => { + const id = `${baseId}-${index}`; + const group: NodeModel = { + id, + children: [], + type: 'group', + group: true, + label: id, + style: { padding: 15 }, + data: { + objectType: 'GN', + } + }; + if (level === groupDepth) { + group.children = childNodes.map(n => n.id); + } else { + const nodesPerChildGroup = Math.floor(childNodes.length / 2); + if (nodesPerChildGroup < 1) { + const g1 = createGroup(childNodes, id, 1, level + 1); + group.children = [g1.id]; + } else { + const g1 = createGroup(childNodes.slice(0, nodesPerChildGroup), id, 1, level + 1); + const g2 = createGroup(childNodes.slice(nodesPerChildGroup), id, 2, level + 1); + group.children = [g1.id, g2.id]; + } + } + + groups.push(group); + return group; + }; + + for (let i = 0; i < numNodes; i++) { + nodes.push(createNode(i)); + } + + const nodesPerGroup = Math.floor((numNodes - 2) / numGroups); + for (let i = 0; i < numGroups; i++) { + createGroup(nodes.slice(i * nodesPerGroup, (i + 1) * nodesPerGroup), 'Group', i + 1); + } + + for (let i = 0; i < numEdges; i++) { + const sourceNum = getRandomNode(numNodes); + const targetNum = getRandomNode(numNodes, sourceNum); + const edge = { + id: `edge-${nodes[sourceNum].id}-${nodes[targetNum].id}`, + type: 'edge', + source: nodes[sourceNum].id, + target: nodes[targetNum].id, + data: { + index: i, + tag: '250kbs', + }, + }; + edges.push(edge); + } + + nodes.push(...groups); + + return { nodes, edges }; +}; diff --git a/packages/demo-app-ts/src/demos/topologyPackageDemo/listeners.ts b/packages/demo-app-ts/src/demos/topologyPackageDemo/listeners.ts new file mode 100644 index 00000000..36a10cbb --- /dev/null +++ b/packages/demo-app-ts/src/demos/topologyPackageDemo/listeners.ts @@ -0,0 +1,45 @@ +import { Controller, EventListener, isNode, Node } from '@patternfly/react-topology'; + +let positionTimer: NodeJS.Timer; + +export const graphPositionChangeListener: EventListener = ({ graph }): void => { + const scale = graph.getScale(); + const position = graph.getPosition(); + const scaleExtent = graph.getScaleExtent(); + + // eslint-disable-next-line no-console + console.log(`Graph Position Change:\n Position: ${Math.round(position.x)},${Math.round(position.y)}\n Scale: ${scale}\n Scale Extent: max: ${scaleExtent[0]} max: ${scaleExtent[1]}`); + + // After an interval, check that what we got was the final value. + if (positionTimer) { + clearTimeout(positionTimer); + } + + positionTimer = setTimeout(() => { + const newScale = graph.getScale(); + const newPosition = graph.getPosition(); + const newScaleExtent = graph.getScaleExtent(); + + // Output an error if any of the graph position values differ from when the last event was fired + if (newScale !== scale) { + // eslint-disable-next-line no-console + console.error(`Scale Changed: ${scale} => ${newScale}`); + } + if (newPosition.x !== position.x || newPosition.y !== position.y) { + // eslint-disable-next-line no-console + console.error(`Graph Position Changed: ${Math.round(position.x)},${Math.round(position.y)} => ${Math.round(newPosition.x)},${Math.round(newPosition.y)}`); + } + if (newScaleExtent !== scaleExtent) { + // eslint-disable-next-line no-console + console.error(`Scale Extent Changed: ${scaleExtent} => ${scaleExtent}`); + } + }, 1000); +}; + +export const layoutEndListener: EventListener = ({ graph }): void => { + const controller: Controller = graph.getController(); + const positions = controller.getElements().filter(e => isNode(e)).map((node) => `Node: ${node.getLabel()}: ${Math.round((node as Node).getPosition().x)},${Math.round((node as Node).getPosition().y)}`); + + // eslint-disable-next-line no-console + console.log(`Layout Complete:\n${positions.join('\n')}`); +}; diff --git a/packages/demo-app-ts/src/index.tsx b/packages/demo-app-ts/src/index.tsx index 079d480a..0c978adb 100755 --- a/packages/demo-app-ts/src/index.tsx +++ b/packages/demo-app-ts/src/index.tsx @@ -4,5 +4,6 @@ import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import '@patternfly/patternfly/patternfly-theme-dark.css'; +import '@patternfly/patternfly/patternfly-addons.css'; ReactDOM.render(, document.getElementById('root')); diff --git a/packages/demo-app-ts/src/utils/usePipelineOptions.tsx b/packages/demo-app-ts/src/utils/usePipelineOptions.tsx deleted file mode 100644 index a28f07cd..00000000 --- a/packages/demo-app-ts/src/utils/usePipelineOptions.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Checkbox, ToolbarItem } from '@patternfly/react-core'; - -export const usePipelineOptions = ( - isLayout = false, -): { - contextToolbar: React.ReactNode; - showContextMenu: boolean; - showBadges: boolean; - showIcons: boolean; - showGroups: boolean; - badgeTooltips: boolean; - verticalLayout: boolean; -} => { - const [showContextMenu, setShowContextMenu] = React.useState(false); - const [showBadges, setShowBadges] = React.useState(false); - const [showIcons, setShowIcons] = React.useState(false); - const [showGroups, setShowGroups] = React.useState(false); - const [verticalLayout, setVerticalLayout] = React.useState(false); - const [badgeTooltips, setBadgeTooltips] = React.useState(false); - - const contextToolbar = ( - <> - - setShowIcons(checked)} label="Show icons" /> - - - setShowBadges(checked)} label="Show badges" /> - - - setBadgeTooltips(checked)} label="Badge tooltips" /> - - - setShowContextMenu(checked)} label="Context menus" /> - - {isLayout ? ( - <> - - setShowGroups(checked)} label="Show groups" /> - - - setVerticalLayout(checked)} label="Vertical layout" /> - - - ) : null} - - ); - - return { contextToolbar, showContextMenu, showBadges, showIcons, showGroups, badgeTooltips, verticalLayout }; -}; diff --git a/packages/demo-app-ts/src/utils/useTopologyOptions.tsx b/packages/demo-app-ts/src/utils/useTopologyOptions.tsx deleted file mode 100644 index 3f8c2bbb..00000000 --- a/packages/demo-app-ts/src/utils/useTopologyOptions.tsx +++ /dev/null @@ -1,555 +0,0 @@ -import React from 'react'; -import { - Button, - Dropdown, - DropdownItem, - DropdownList, - Flex, - MenuToggle, - MenuToggleElement, - Select, - SelectList, - SelectOption, - Split, - SplitItem, - TextInput, - ToolbarItem, - Tooltip -} from '@patternfly/react-core'; -import { DefaultEdgeOptions, DefaultNodeOptions, GeneratorEdgeOptions, GeneratorNodeOptions } from '../data/generator'; -import { EDGE_ANIMATION_SPEEDS, EDGE_STYLES, EDGE_TERMINAL_TYPES, NODE_SHAPES, NODE_STATUSES } from './styleUtils'; -import { Controller, Model, NodeShape } from '@patternfly/react-topology'; - -export const useTopologyOptions = ( - controller: Controller -): { - layout: string; - nodeOptions: GeneratorNodeOptions; - edgeOptions: GeneratorEdgeOptions; - nestedLevel: number; - creationCounts: { numNodes: number; numEdges: number; numGroups: number }; - medScale: number; - lowScale: number; - contextToolbar: React.ReactNode; - viewToolbar: React.ReactNode; -} => { - 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 updateLayout = (newLayout: string) => { - setLayout(newLayout); - setLayoutDropdownOpen(false); - }; - - const layoutDropdown = ( - - - - - - ) => ( - setLayoutDropdownOpen(!layoutDropdownOpen)}> - {layout} - - )} - isOpen={layoutDropdownOpen} - onOpenChange={(isOpen) => setLayoutDropdownOpen(isOpen)} - > - - updateLayout('Force')}> - Force - - updateLayout('Dagre')}> - Dagre - - updateLayout('Cola')}> - Cola - - updateLayout('ColaGroups')}> - ColaGroups - - updateLayout('ColaNoForce')}> - ColaNoForce - - updateLayout('Grid')}> - Grid - - updateLayout('Concentric')}> - Concentric - - updateLayout('BreadthFirst')}> - BreadthFirst - - - - - - ); - - const renderNodeOptionsDropdown = () => { - const selectContent = ( - - setNodeOptions((prev) => ({ ...prev, nodeLabels: !prev.nodeLabels }))} - > - Labels - - setNodeOptions((prev) => ({ ...prev, nodeSecondaryLabels: !prev.nodeSecondaryLabels }))} - > - Secondary Labels - - 1} - onClick={() => - setNodeOptions((prev) => ({ - ...prev, - statuses: prev.statuses.length > 1 ? DefaultNodeOptions.statuses : NODE_STATUSES - })) - } - > - Status - - - setNodeOptions((prev) => ({ - ...prev, - statusDecorators: !prev.statusDecorators, - showDecorators: !prev.showDecorators - })) - } - > - Decorators - - setNodeOptions((prev) => ({ ...prev, nodeBadges: !prev.nodeBadges }))} - > - Badges - - setNodeOptions((prev) => ({ ...prev, nodeIcons: !prev.nodeIcons }))} - > - Icons - - setNodeOptions((prev) => ({ ...prev, contextMenus: !prev.contextMenus }))} - > - Context Menus - - setNodeOptions((prev) => ({ ...prev, hulledOutline: !prev.hulledOutline }))} - > - Rectangle Groups - - - ); - - const nodeOptionsToggle = (toggleRef: React.Ref) => ( - setNodeOptionsOpen((prev) => !prev)} - isExpanded={nodeOptionsOpen} - style={ - { - width: '250px' - } as React.CSSProperties - } - > - Node options - - ); - - return ( - - ); - }; - - 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)} - > - {shape} - - ))} - - ); - - const nodeShapesToggle = (toggleRef: React.Ref) => ( - setNodeShapesOpen((prev) => !prev)} - isExpanded={nodeShapesOpen} - style={ - { - width: '250px' - } as React.CSSProperties - } - > - Node shapes - - ); - - return ( - - ); - }; - - const renderEdgeOptionsDropdown = () => { - const selectContent = ( - - 1} - onClick={() => - setEdgeOptions((prev) => ({ - ...prev, - edgeStatuses: prev.edgeStatuses.length > 1 ? DefaultEdgeOptions.edgeStatuses : NODE_STATUSES - })) - } - > - Status - - 1} - onClick={() => - setEdgeOptions((prev) => ({ - ...prev, - edgeStyles: prev.edgeStyles.length > 1 ? DefaultEdgeOptions.edgeStyles : EDGE_STYLES - })) - } - > - Styles - - 1} - onClick={() => - setEdgeOptions((prev) => ({ - ...prev, - edgeAnimations: prev.edgeAnimations.length > 1 ? DefaultEdgeOptions.edgeAnimations : EDGE_ANIMATION_SPEEDS - })) - } - > - Animations - - 1} - onClick={() => - setEdgeOptions((prev) => ({ - ...prev, - terminalTypes: prev.terminalTypes.length > 1 ? DefaultEdgeOptions.terminalTypes : EDGE_TERMINAL_TYPES - })) - } - > - Terminal type - - setEdgeOptions((prev) => ({ ...prev, edgeTags: !prev.edgeTags }))} - > - Tags - - - ); - const edgeOptionsToggle = (toggleRef: React.Ref) => ( - setEdgeOptionsOpen((prev) => !prev)} - isExpanded={edgeOptionsOpen} - style={ - { - width: '250px' - } as React.CSSProperties - } - > - Edge options - - ); - - return ( - - ); - }; - - const saveModel = () => { - setSavedModel(controller.toModel()); - setModelSaved(true); - setTimeout(() => { - setModelSaved(false); - }, 2000); - }; - - const restoreLayout = () => { - if (savedModel) { - const currentModel = controller.toModel(); - currentModel.graph = { - ...currentModel.graph, - x: savedModel.graph.x, - y: savedModel.graph.y, - visible: savedModel.graph.visible, - style: savedModel.graph.style, - layout: savedModel.graph.layout, - scale: savedModel.graph.scale, - scaleExtent: savedModel.graph.scaleExtent, - layers: savedModel.graph.layers, - }; - currentModel.nodes = currentModel.nodes.map((n) => { - const savedNode = savedModel.nodes.find((sn) => sn.id === n.id); - if (!savedNode) { - return n; - } - return { - ...n, - x: savedNode.x, - y: savedNode.y, - visible: savedNode.visible, - style: savedNode.style, - collapsed: savedNode.collapsed, - width: savedNode.width, - height: savedNode.height, - shape: savedNode.shape, - }; - }); - 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/module/src/components/edges/DefaultEdge.tsx b/packages/module/src/components/edges/DefaultEdge.tsx index 927cf03e..84bcee69 100644 --- a/packages/module/src/components/edges/DefaultEdge.tsx +++ b/packages/module/src/components/edges/DefaultEdge.tsx @@ -1,6 +1,15 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Edge, EdgeTerminalType, GraphElement, isEdge, isNode, NodeStatus, ScaleDetailsLevel } from '../../types'; +import { + Edge, + EdgeStyle, + EdgeTerminalType, + GraphElement, + isEdge, + isNode, + NodeStatus, + ScaleDetailsLevel +} from '../../types'; import { ConnectDragSource, OnSelect } from '../../behavior'; import { getClosestVisibleParent, useHover } from '../../utils'; import { Layer } from '../layers'; @@ -22,6 +31,8 @@ interface DefaultEdgeProps { element: GraphElement; /** Flag indicating if the user is dragging the edge */ dragging?: boolean; + /** The style of the edge. Defaults to the style set on the Edge's model */ + edgeStyle?: EdgeStyle; /** The duration in seconds for the edge animation. Defaults to the animationSpeed set on the Edge's model */ animationDuration?: number; /** The terminal type to use for the edge start */ @@ -71,6 +82,7 @@ const DefaultEdgeInner: React.FunctionComponent = observe dragging, sourceDragRef, targetDragRef, + edgeStyle, animationDuration, onShowRemoveConnector, onHideRemoveConnector, @@ -123,7 +135,7 @@ const DefaultEdgeInner: React.FunctionComponent = observe ); const edgeAnimationDuration = animationDuration ?? getEdgeAnimationDuration(element.getEdgeAnimationSpeed()); - const linkClassName = css(styles.topologyEdgeLink, getEdgeStyleClassModifier(element.getEdgeStyle())); + const linkClassName = css(styles.topologyEdgeLink, getEdgeStyleClassModifier(edgeStyle || element.getEdgeStyle())); const bendpoints = element.getBendpoints(); diff --git a/packages/module/src/components/groups/DefaultGroup.tsx b/packages/module/src/components/groups/DefaultGroup.tsx index 3c5977f0..88599d57 100644 --- a/packages/module/src/components/groups/DefaultGroup.tsx +++ b/packages/module/src/components/groups/DefaultGroup.tsx @@ -8,6 +8,8 @@ import { Dimensions } from '../../geom'; import DefaultGroupCollapsed from './DefaultGroupCollapsed'; interface DefaultGroupProps { + /** Additional content added to the node */ + children?: React.ReactNode; /** Additional classes added to the group */ className?: string; /** The graph group node element to represent */