diff --git a/packages/demo-app-ts/src/components/GroupHull.tsx b/packages/demo-app-ts/src/components/GroupHull.tsx index 423d2ce6..1d684db3 100644 --- a/packages/demo-app-ts/src/components/GroupHull.tsx +++ b/packages/demo-app-ts/src/components/GroupHull.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import { polygonHull } from 'd3-polygon'; -import * as _ from 'lodash'; import { WithDragNodeProps, WithSelectionProps, @@ -85,7 +84,7 @@ const GroupHull: React.FunctionComponent = ({ return null; } const points: PointWithSize[] = []; - _.forEach(nodeChildren, c => { + nodeChildren.forEach(c => { if (c.getNodeShape() === NodeShape.ellipse) { const { width, height } = c.getBounds(); const { x, y } = c.getBounds().getCenter(); diff --git a/packages/demo-app-ts/src/demos/TopologyPackage.tsx b/packages/demo-app-ts/src/demos/TopologyPackage.tsx index 67f27cdc..d11e11a0 100644 --- a/packages/demo-app-ts/src/demos/TopologyPackage.tsx +++ b/packages/demo-app-ts/src/demos/TopologyPackage.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { action } from 'mobx'; -import * as _ from 'lodash'; import { Controller, createTopologyControlButtons, @@ -146,8 +145,8 @@ const TopologyViewComponent: React.FunctionComponent }, [controller, lowScale, medScale]); const topologySideBar = ( - 0} resizable={sideBarResizable} onClose={() => setSelectedIds([])}> -
{_.head(selectedIds)}
+ setSelectedIds([])}> +
{selectedIds?.[0]}
); @@ -208,7 +207,7 @@ const TopologyViewComponent: React.FunctionComponent contextToolbar={contextToolbar} viewToolbar={viewToolbar} sideBar={useSidebar && topologySideBar} - sideBarOpen={useSidebar && _.size(selectedIds) > 0} + sideBarOpen={useSidebar && !!selectedIds?.length} sideBarResizable={sideBarResizable} > diff --git a/packages/demo-app-ts/src/utils/styleUtils.ts b/packages/demo-app-ts/src/utils/styleUtils.ts index 2035ef57..cc0b33ea 100644 --- a/packages/demo-app-ts/src/utils/styleUtils.ts +++ b/packages/demo-app-ts/src/utils/styleUtils.ts @@ -137,15 +137,18 @@ export const createNode = (options: { (options.marginX || 60) + (options.x ?? (options.column - 1) * - (options.label && options.labelPosition === LabelPosition.right ? RIGHT_LABEL_COLUMN_WIDTH : COLUMN_WIDTH)); + (options.label && [LabelPosition.right, LabelPosition.left].includes(options.labelPosition) + ? RIGHT_LABEL_COLUMN_WIDTH + : COLUMN_WIDTH)); nodeModel.y = 20 + (width - height) / 2 + (options.y ?? (options.row - 1) * - (!options.label || options.labelPosition === LabelPosition.right ? ROW_HEIGHT : BOTTOM_LABEL_ROW_HEIGHT)); + (!options.label || [LabelPosition.right, LabelPosition.left].includes(options.labelPosition) + ? ROW_HEIGHT + : BOTTOM_LABEL_ROW_HEIGHT)); } - return nodeModel; }; diff --git a/packages/demo-app-ts/src/utils/useTopologyOptions.tsx b/packages/demo-app-ts/src/utils/useTopologyOptions.tsx index 5c7c23a5..3f8c2bbb 100644 --- a/packages/demo-app-ts/src/utils/useTopologyOptions.tsx +++ b/packages/demo-app-ts/src/utils/useTopologyOptions.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import * as _ from 'lodash'; import { Button, Dropdown, @@ -21,9 +20,6 @@ import { DefaultEdgeOptions, DefaultNodeOptions, GeneratorEdgeOptions, Generator import { EDGE_ANIMATION_SPEEDS, EDGE_STYLES, EDGE_TERMINAL_TYPES, NODE_SHAPES, NODE_STATUSES } from './styleUtils'; import { Controller, Model, NodeShape } from '@patternfly/react-topology'; -const GRAPH_LAYOUT_OPTIONS = ['x', 'y', 'visible', 'style', 'layout', 'scale', 'scaleExtent', 'layers']; -const NODE_LAYOUT_OPTIONS = ['x', 'y', 'visible', 'style', 'collapsed', 'width', 'height', 'shape']; - export const useTopologyOptions = ( controller: Controller ): { @@ -380,7 +376,14 @@ export const useTopologyOptions = ( const currentModel = controller.toModel(); currentModel.graph = { ...currentModel.graph, - ..._.pick(savedModel.graph, GRAPH_LAYOUT_OPTIONS) + 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); @@ -389,7 +392,14 @@ export const useTopologyOptions = ( } return { ...n, - ..._.pick(savedNode, NODE_LAYOUT_OPTIONS) + 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); diff --git a/packages/module/package.json b/packages/module/package.json index c669849b..3a36eb34 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -42,7 +42,6 @@ "@types/react-measure": "^2.0.6", "d3": "^7.8.0", "dagre": "0.8.2", - "lodash": "^4.17.19", "mobx": "^6.9.0", "mobx-react": "^7.6.0", "point-in-svg-path": "^1.0.1", @@ -61,7 +60,6 @@ "@patternfly/patternfly-a11y": "^4.3.1", "@patternfly/react-code-editor": "^5.1.1", "@patternfly/react-table": "^5.1.1", - "@types/lodash": "^4.14.191", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "camel-case": "^3.0.0", diff --git a/packages/module/src/Visualization.ts b/packages/module/src/Visualization.ts index b9857d0c..41e914de 100644 --- a/packages/module/src/Visualization.ts +++ b/packages/module/src/Visualization.ts @@ -1,6 +1,5 @@ import { ComponentType } from 'react'; -import { action, computed, observable, makeObservable } from 'mobx'; -import * as _ from 'lodash'; +import { action, computed, observable, makeObservable, configure } from 'mobx'; import { Controller, Graph, @@ -24,6 +23,9 @@ import { import defaultElementFactory from './elements/defaultElementFactory'; import Stateful from './utils/Stateful'; +// Configure MobX to isolate state, this allows for applications to use different versions of MobX +configure({ isolateGlobalState: true }); + export class Visualization extends Stateful implements Controller { elements: { [id: string]: GraphElement } = {}; @@ -94,7 +96,7 @@ export class Visualization extends Stateful implements Controller { // If not merging, clear out the old elements if (!merge) { - _.forIn(this.elements, element => this.removeElement(element)); + Object.keys(this.elements).forEach(element => this.removeElement(this.elements[element])); } // Create the graph if given in the model @@ -147,7 +149,8 @@ export class Visualization extends Stateful implements Controller { // remove all stale elements if (merge) { - _.forIn(this.elements, element => { + Object.keys(this.elements).forEach(key => { + const element = this.elements[key]; if (!validIds.includes(element.getId())) { this.removeElement(element); } @@ -186,7 +189,7 @@ export class Visualization extends Stateful implements Controller { } getElements(): GraphElement[] { - return _.values(this.elements); + return Object.values(this.elements); } toModel(): Model { diff --git a/packages/module/src/components/VisualizationSurface.tsx b/packages/module/src/components/VisualizationSurface.tsx index 38c38846..66be3a76 100644 --- a/packages/module/src/components/VisualizationSurface.tsx +++ b/packages/module/src/components/VisualizationSurface.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; -import * as _ from 'lodash'; import { action } from 'mobx'; // https://github.com/mobxjs/mobx-react#observer-batching import 'mobx-react/batchingForReactDom'; import { observer } from 'mobx-react'; -import ReactMeasure from 'react-measure'; +import ReactMeasure, { ContentRect } from 'react-measure'; import { css } from '@patternfly/react-styles'; import styles from '../css/topology-components'; import { State } from '../types'; @@ -29,6 +28,18 @@ const VisualizationSurface: React.FunctionComponent = state }: VisualizationSurfaceProps) => { const controller = useVisualizationController(); + const timerId = React.useRef(); + + const debounceMeasure = React.useCallback((func: (contentRect: ContentRect) => void, delay?: number) => { + return (contentRect: ContentRect) => { + if (!timerId.current) { + func(contentRect) + } + clearTimeout(timerId.current) + + timerId.current = setTimeout(() => func(contentRect), delay) + } + }, []); React.useEffect(() => { state && controller.setState(state); @@ -36,18 +47,17 @@ const VisualizationSurface: React.FunctionComponent = const onMeasure = React.useMemo( () => - _.debounce( - action((contentRect: { client: { width: number; height: number } }) => { + debounceMeasure( + action((contentRect: ContentRect) => { controller.getGraph().setDimensions(new Dimensions(contentRect.client.width, contentRect.client.height)); }), 100, - { leading: true, trailing: true } ), - [controller] + [controller, debounceMeasure] ); // dispose of onMeasure - React.useEffect(() => () => onMeasure.cancel(), [onMeasure]); + React.useEffect(() => () => clearTimeout(timerId.current), [onMeasure]); if (!controller.hasGraph()) { return null; diff --git a/packages/module/src/components/edges/DefaultEdge.tsx b/packages/module/src/components/edges/DefaultEdge.tsx index f5196012..927cf03e 100644 --- a/packages/module/src/components/edges/DefaultEdge.tsx +++ b/packages/module/src/components/edges/DefaultEdge.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import * as _ from 'lodash'; import { observer } from 'mobx-react'; import { Edge, EdgeTerminalType, GraphElement, isEdge, isNode, NodeStatus, ScaleDetailsLevel } from '../../types'; import { ConnectDragSource, OnSelect } from '../../behavior'; @@ -134,15 +133,15 @@ const DefaultEdgeInner: React.FunctionComponent = observe const bgStartPoint = !startTerminalType || startTerminalType === EdgeTerminalType.none ? [startPoint.x, startPoint.y] - : getConnectorStartPoint(_.head(bendpoints) || endPoint, startPoint, startTerminalSize); + : getConnectorStartPoint(bendpoints?.[0] || endPoint, startPoint, startTerminalSize); const bgEndPoint = !endTerminalType || endTerminalType === EdgeTerminalType.none ? [endPoint.x, endPoint.y] - : getConnectorStartPoint(_.last(bendpoints) || startPoint, endPoint, endTerminalSize); + : getConnectorStartPoint(bendpoints?.[bendpoints.length - 1] || startPoint, endPoint, endTerminalSize); const backgroundPath = `M${bgStartPoint[0]} ${bgStartPoint[1]} ${bendpoints .map((b: Point) => `L${b.x} ${b.y} `) .join('')}L${bgEndPoint[0]} ${bgEndPoint[1]}`; - + const showTag = tag && (detailsLevel === ScaleDetailsLevel.high || hover); const scale = element.getGraph().getScale(); const tagScale = hover && !(detailsLevel === ScaleDetailsLevel.high) ? Math.max(1, 1 / scale) : 1; diff --git a/packages/module/src/components/edges/terminals/ConnectorArrow.tsx b/packages/module/src/components/edges/terminals/ConnectorArrow.tsx index 43408e8e..c00d4646 100644 --- a/packages/module/src/components/edges/terminals/ConnectorArrow.tsx +++ b/packages/module/src/components/edges/terminals/ConnectorArrow.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import * as _ from 'lodash'; import { css } from '@patternfly/react-styles'; import styles from '../../../css/topology-components'; import Point from '../../../geom/Point'; @@ -15,8 +14,8 @@ interface ConnectorArrowProps { dragRef?: ConnectDragSource; } -const pointsStringFromPoints = (points: [number, number][]): string => - _.reduce(points, (result: string, nextPoint: [number, number]) => `${result} ${nextPoint[0]},${nextPoint[1]}`, ''); +const pointsStringFromPoints = (points?: [number, number][]): string => + points?.reduce((result: string, nextPoint: [number, number]) => `${result} ${nextPoint[0]},${nextPoint[1]}`, '') ?? ''; const ConnectorArrow: React.FunctionComponent = ({ startPoint, diff --git a/packages/module/src/components/edges/terminals/DefaultConnectorTerminal.tsx b/packages/module/src/components/edges/terminals/DefaultConnectorTerminal.tsx index 8df1831f..0c22d67e 100644 --- a/packages/module/src/components/edges/terminals/DefaultConnectorTerminal.tsx +++ b/packages/module/src/components/edges/terminals/DefaultConnectorTerminal.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import * as _ from 'lodash'; import { css } from '@patternfly/react-styles'; import styles from '../../../css/topology-components'; import { Edge, EdgeTerminalType, NodeStatus } from '../../../types'; @@ -55,7 +54,7 @@ const DefaultConnectorTerminal: React.FunctionComponent return null; } const bendPoints = edge.getBendpoints(); - const startPoint = isTarget ? _.last(bendPoints) || edge.getStartPoint() : _.head(bendPoints) || edge.getEndPoint(); + const startPoint = isTarget ? bendPoints[bendPoints.length - 1] || edge.getStartPoint() : bendPoints[0] || edge.getEndPoint(); const endPoint = isTarget ? edge.getEndPoint() : edge.getStartPoint(); const classes = css(styles.topologyEdge, className, StatusModifier[status]); diff --git a/packages/module/src/components/edges/terminals/terminalUtils.ts b/packages/module/src/components/edges/terminals/terminalUtils.ts index 0846527d..f9d08bca 100644 --- a/packages/module/src/components/edges/terminals/terminalUtils.ts +++ b/packages/module/src/components/edges/terminals/terminalUtils.ts @@ -13,7 +13,7 @@ export const getConnectorStartPoint = (startPoint: Point, endPoint: Point, size: export const getConnectorRotationAngle = (startPoint: Point, endPoint: Point): number => 180 - (Math.atan2(endPoint.y - startPoint.y, startPoint.x - endPoint.x) * 180) / Math.PI; -export const getConnectorBoundingBox = (startPoint: Point, endPoint: Point, size: number): [number, number][] => { +export const getConnectorBoundingBox = (startPoint: Point, endPoint: Point, size: number): [number, number][] | null => { const length = Math.sqrt((endPoint.x - startPoint.x) ** 2 + (endPoint.y - startPoint.y) ** 2); if (!length) { return null; diff --git a/packages/module/src/components/groups/DefaultGroup.tsx b/packages/module/src/components/groups/DefaultGroup.tsx index 1beab616..b8515c88 100644 --- a/packages/module/src/components/groups/DefaultGroup.tsx +++ b/packages/module/src/components/groups/DefaultGroup.tsx @@ -29,7 +29,7 @@ interface DefaultGroupProps { secondaryLabel?: string; /** Flag to show the label */ showLabel?: boolean; // Defaults to true - /** Position of the label, bottom or left. Defaults to element.getLabelPosition() or bottom */ + /** Position of the label, top or bottom. Defaults to element.getLabelPosition() or bottom */ labelPosition?: LabelPosition; /** The maximum length of the label before truncation */ truncateLength?: number; diff --git a/packages/module/src/components/groups/DefaultGroupCollapsed.tsx b/packages/module/src/components/groups/DefaultGroupCollapsed.tsx index 0dcffbbb..e2265280 100644 --- a/packages/module/src/components/groups/DefaultGroupCollapsed.tsx +++ b/packages/module/src/components/groups/DefaultGroupCollapsed.tsx @@ -151,8 +151,8 @@ const DefaultGroupCollapsed: React.FunctionComponent {showLabel && ( { + if (labelPosition === LabelPosition.top) { + points.forEach((p) => { + const delta = !highPoints ? -Infinity : Math.round(p[1]) - Math.round(highPoints[0][1]); + // If the difference is greater than the threshold, update the highest point + if (delta < -threshold) { + highPoints = [p]; + } else if (Math.abs(delta) <= threshold) { + if (!highPoints) { + highPoints = []; + } + highPoints.push(p); + } + }); + + // find min and max by x and y coordinates + const minX = highPoints.reduce((min, p) => Math.min(min, p[0]), Infinity); + const maxX = highPoints.reduce((max, p) => Math.max(max, p[0]), -Infinity); + const minY = highPoints.reduce((min, p) => Math.min(min, p[1]), Infinity); + // find max by size value + const maxSize = highPoints.reduce((max, p) => Math.max(max, p[2]), -Infinity); + + return [ + (minX + maxX) / 2, + minY, + // use the max size value + maxSize + ]; + } + + points.forEach(p => { const delta = !lowPoints ? Infinity : Math.round(p[1]) - Math.round(lowPoints[0][1]); if (delta > threshold) { lowPoints = [p]; @@ -62,11 +96,20 @@ export function computeLabelLocation(points: PointWithSize[]): PointWithSize { lowPoints.push(p); } }); + + const minX = lowPoints.reduce((acc, point) => { + return Math.min(acc, point[0]); + }, Number.POSITIVE_INFINITY); + const maxX = lowPoints.reduce((acc, point) => { + return Math.max(acc, point[0]); + }, Number.NEGATIVE_INFINITY); + const maxSize = lowPoints.reduce((acc, point) => { + return Math.max(acc, point[2]); + }, Number.NEGATIVE_INFINITY); return [ - (_.minBy(lowPoints, p => p[0])[0] + _.maxBy(lowPoints, p => p[0])[0]) / 2, + (minX + maxX) / 2, lowPoints[0][1], - // use the max size value - _.maxBy(lowPoints, p => p[2])[2] + maxSize, ]; } @@ -97,6 +140,7 @@ const DefaultGroupExpanded: React.FunctionComponent = badgeLocation, labelIconClass, labelIcon, + labelPosition, labelIconPadding, onCollapseChange, hulledOutline = true, @@ -129,7 +173,7 @@ const DefaultGroupExpanded: React.FunctionComponent = return null; } const points: (PointWithSize | PointTuple)[] = []; - _.forEach(children, c => { + children.forEach(c => { if (c.getNodeShape() === NodeShape.circle) { const bounds = c.getBounds(); const { width, height } = bounds; @@ -157,10 +201,13 @@ const DefaultGroupExpanded: React.FunctionComponent = pathRef.current = hullPath(hullPoints as PointTuple[], hullPadding); // Compute the location of the group label. - labelLocation.current = computeLabelLocation(hullPoints as PointWithSize[]); + labelLocation.current = computeLabelLocation(hullPoints as PointWithSize[], labelPosition); } else { boxRef.current = element.getBounds(); - labelLocation.current = [boxRef.current.x + boxRef.current.width / 2, boxRef.current.y + boxRef.current.height, 0]; + labelLocation.current = + labelPosition === LabelPosition.top + ? [boxRef.current.x + boxRef.current.width / 2, boxRef.current.y, 0] + : [boxRef.current.x + boxRef.current.width / 2, boxRef.current.y + boxRef.current.height, 0]; } } @@ -183,6 +230,14 @@ const DefaultGroupExpanded: React.FunctionComponent = canDrop && dropTarget && 'pf-m-drop-target' ); + const outlinePadding = hulledOutline ? hullPadding(labelLocation.current) : 0; + const labelGap = 24; + const startX = labelLocation.current[0]; + const startY = + labelPosition === LabelPosition.top + ? labelLocation.current[1] - outlinePadding - labelGap * 2 + : labelLocation.current[1] + outlinePadding + labelGap; + return ( @@ -198,8 +253,8 @@ const DefaultGroupExpanded: React.FunctionComponent = = observe return { translateX, translateY }; }, [element, nodeScale, scaleNode]); + let labelX; + let labelY; + const labelPaddingX = 8; + const labelPaddingY = 4; + if(nodeLabelPosition === LabelPosition.right) { + labelX = (width + labelPaddingX) * labelPositionScale; + labelY = height / 2; + } else if (nodeLabelPosition === LabelPosition.left) { + labelX = 0; + labelY = height / 2 - labelPaddingY; + } else if(nodeLabelPosition === LabelPosition.top) { + labelX = width / 2; + labelY = labelPaddingY + labelPaddingY/2; + } else { + labelX = width / 2 * labelPositionScale; + labelY = height + labelPaddingY + labelPaddingY / 2 ; + } return ( = observe = ({ const primaryWidth = iconSpace + badgeSpace + paddingX + textSize.width + actionSpace + contextSpace + paddingX; const secondaryWidth = secondaryLabel && secondaryTextSize ? secondaryTextSize.width + 2 * paddingX : 0; const width = Math.max(primaryWidth, secondaryWidth); - const startX = position === LabelPosition.right ? x + iconSpace : x - width / 2 + iconSpace / 2; - const startY = position === LabelPosition.right ? y - height / 2 : y; + + let startX: number; + let startY: number; + if (position === LabelPosition.top) { + startX = x - width / 2; + startY = -y - height - paddingY; + } else if (position === LabelPosition.right) { + startX = x + iconSpace; + startY = y - height / 2; + } else if (position === LabelPosition.left) { + startX = - width - paddingX; + startY = y - height / 2 + paddingY; + } else { + startX = x - width / 2 + iconSpace / 2; + startY = y; + } const actionStartX = iconSpace + badgeSpace + paddingX + textSize.width + paddingX; const contextStartX = actionStartX + actionSpace; const backgroundHeight = diff --git a/packages/module/src/elements/BaseElement.ts b/packages/module/src/elements/BaseElement.ts index 4f3ae176..4d389f86 100644 --- a/packages/module/src/elements/BaseElement.ts +++ b/packages/module/src/elements/BaseElement.ts @@ -1,5 +1,4 @@ import { observable, computed, makeObservable } from 'mobx'; -import * as _ from 'lodash'; import { ElementModel, Graph, @@ -60,7 +59,10 @@ export default abstract class BaseElement + a.length === b.length && a.every((val, i) => val === b[i]) + }) }); } @@ -255,10 +257,10 @@ export default abstract class BaseElement this.removeChild(child)); + this.children.filter(c => !childElements.includes(c)).forEach(child => this.removeChild(child)); // add children - const toAdd = _.difference(childElements, this.children); + const toAdd = childElements.filter(c => !this.children.includes(c)); toAdd.reverse().forEach(child => this.insertChild(child, 0)); } if ('data' in model) { @@ -268,7 +270,7 @@ export default abstract class BaseElement n.id === node.getId()); + if (!layoutNode && node.getNodes().length) { + const id = node.getChildren()[0].getId(); + layoutNode = nodes.find(n => n.id === id); } if (!layoutNode) { layoutNode = this.getLayoutNode(nodes, getClosestVisibleParent(node)); diff --git a/packages/module/src/layouts/DagreLayout.ts b/packages/module/src/layouts/DagreLayout.ts index cf0a6225..47eb11ae 100644 --- a/packages/module/src/layouts/DagreLayout.ts +++ b/packages/module/src/layouts/DagreLayout.ts @@ -1,5 +1,4 @@ import * as dagre from 'dagre'; -import * as _ from 'lodash'; import { Edge, Graph, GRAPH_LAYOUT_END_EVENT, Layout, Node } from '../types'; import { BaseLayout, LAYOUT_DEFAULTS } from './BaseLayout'; import { LayoutOptions } from './LayoutOptions'; @@ -42,7 +41,7 @@ export class DagreLayout extends BaseLayout implements Layout { } protected updateEdgeBendpoints(edges: DagreLink[]): void { - _.forEach(edges, edge => { + edges.forEach(edge => { const link = edge as DagreLink; link.updateBendpoints(); }); @@ -55,26 +54,26 @@ export class DagreLayout extends BaseLayout implements Layout { protected startLayout(graph: Graph, initialRun: boolean, addingNodes: boolean): void { if (initialRun || addingNodes) { const dagreGraph = new dagre.graphlib.Graph({ compound: true }); - dagreGraph.setGraph(_.omit(this.dagreOptions, Object.keys(LAYOUT_DEFAULTS))); + const options = { ...this.dagreOptions }; + Object.keys(LAYOUT_DEFAULTS).forEach(key => delete options[key]); + dagreGraph.setGraph(options); if (!this.dagreOptions.ignoreGroups) { - _.forEach(this.groups, group => { + this.groups.forEach(group => { dagreGraph.setNode(group.id, group); dagreGraph.setParent(group.id, group.element.getParent().getId()); }); } - const updatedNodes: dagre.Node[] = []; - _.forEach(this.nodes, node => { + this.nodes?.forEach(node => { const updateNode = (node as DagreNode).getUpdatableNode(); - updatedNodes.push(updateNode); dagreGraph.setNode(node.id, updateNode); if (!this.dagreOptions.ignoreGroups) { dagreGraph.setParent(node.id, node.element.getParent().getId()); } }); - _.forEach(this.edges, dagreEdge => { + this.edges?.forEach(dagreEdge => { dagreGraph.setEdge(dagreEdge.source.id, dagreEdge.target.id, dagreEdge); }); diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx index d711e264..70b8b6f6 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -117,6 +117,8 @@ const DefaultTaskGroupInner: React.FunctionComponent return [0, 0]; } switch (labelPosition) { + case LabelPosition.top: + return [minX + (maxX - minX) / 2, -minY + labelOffset]; case LabelPosition.right: return [maxX + labelOffset, minY + (maxY - minY) / 2]; case LabelPosition.bottom: diff --git a/packages/module/src/types.ts b/packages/module/src/types.ts index 4385a6e7..d488513b 100644 --- a/packages/module/src/types.ts +++ b/packages/module/src/types.ts @@ -84,6 +84,8 @@ export enum EdgeTerminalType { } export enum LabelPosition { + top, + left, right, bottom } diff --git a/packages/module/src/utils/createAggregateEdges.ts b/packages/module/src/utils/createAggregateEdges.ts index 35e7155c..c7b4b764 100644 --- a/packages/module/src/utils/createAggregateEdges.ts +++ b/packages/module/src/utils/createAggregateEdges.ts @@ -1,4 +1,3 @@ -import * as lodash from 'lodash'; import { EdgeModel, NodeModel } from '../types'; const getNodeParent = (nodeId: string, nodes: NodeModel[]): NodeModel | undefined => @@ -25,10 +24,12 @@ const createAggregateEdges = ( edges: EdgeModel[] | undefined, nodes: NodeModel[] | undefined ): EdgeModel[] => { + if (!edges) { + return []; + } const aggregateEdges: EdgeModel[] = []; - return lodash.reduce( - edges, + return edges.reduce( (newEdges: EdgeModel[], edge: EdgeModel) => { const source = getDisplayedNodeForNode(edge.source, nodes); const target = getDisplayedNodeForNode(edge.target, nodes); @@ -50,7 +51,7 @@ const createAggregateEdges = ( edge.visible = false; // Hide edges that are depicted by this aggregate edge - lodash.forEach(existing.children, existingChild => { + existing.children?.forEach(existingChild => { const updateEdge = newEdges.find(newEdge => newEdge.id === existingChild); if (updateEdge) { updateEdge.visible = false; diff --git a/packages/module/src/utils/element-utils.ts b/packages/module/src/utils/element-utils.ts index 6248d182..93d9502f 100644 --- a/packages/module/src/utils/element-utils.ts +++ b/packages/module/src/utils/element-utils.ts @@ -1,12 +1,11 @@ -import * as _ from 'lodash'; import { GraphElement, Node, isNode, isGraph, NodeStyle } from '../types'; const groupNodeElements = (nodes: GraphElement[]): Node[] => { - if (!_.size(nodes)) { + if (!nodes.length) { return []; } const groupNodes: Node[] = []; - _.forEach(nodes, nextNode => { + nodes.forEach(nextNode => { if (isNode(nextNode) && nextNode.isGroup() && !nextNode.isCollapsed()) { groupNodes.push(nextNode); groupNodes.push(...groupNodeElements(nextNode.getChildren())); @@ -23,7 +22,7 @@ const leafNodeElements = (nodeElements: Node | Node[] | null): Node[] => { } if (Array.isArray(nodeElements)) { - _.forEach(nodeElements, (nodeElement: Node) => { + nodeElements.forEach((nodeElement: Node) => { nodes.push(...leafNodeElements(nodeElement)); }); return nodes; @@ -31,15 +30,8 @@ const leafNodeElements = (nodeElements: Node | Node[] | null): Node[] => { if (nodeElements.isGroup() && !nodeElements.isCollapsed()) { const leafNodes: Node[] = []; - const children: GraphElement[] = nodeElements.getChildren(); - if (_.size(children)) { - _.forEach( - children.filter(e => isNode(e)), - element => { - leafNodes.push(...leafNodeElements(element as Node)); - } - ); - } + const children: GraphElement[] = nodeElements.getChildren().filter(e => isNode(e)); + children.forEach(element => leafNodes.push(...leafNodeElements(element as Node))); return leafNodes; } diff --git a/yarn.lock b/yarn.lock index 50967a9a..41a65bb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2442,11 +2442,6 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.14.191": - version "4.14.191" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz" - integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== - "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz"