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/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..3b39224e 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 { Controller, Graph, @@ -94,7 +93,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 +146,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 +186,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..a3898394 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'; @@ -16,7 +15,7 @@ interface ConnectorArrowProps { } const pointsStringFromPoints = (points: [number, number][]): string => - _.reduce(points, (result: string, nextPoint: [number, number]) => `${result} ${nextPoint[0]},${nextPoint[1]}`, ''); + 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..a1cc8c83 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/groups/DefaultGroupExpanded.tsx b/packages/module/src/components/groups/DefaultGroupExpanded.tsx index e3b5fc94..511d48a4 100644 --- a/packages/module/src/components/groups/DefaultGroupExpanded.tsx +++ b/packages/module/src/components/groups/DefaultGroupExpanded.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 { css } from '@patternfly/react-styles'; import styles from '../../css/topology-components'; import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-alt-icon'; @@ -54,7 +53,7 @@ export function computeLabelLocation(points: PointWithSize[]): PointWithSize { let lowPoints: PointWithSize[]; const threshold = 5; - _.forEach(points, p => { + points?.forEach(p => { const delta = !lowPoints ? Infinity : Math.round(p[1]) - Math.round(lowPoints[0][1]); if (delta > threshold) { lowPoints = [p]; @@ -62,11 +61,19 @@ 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, ]; } @@ -129,7 +136,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; diff --git a/packages/module/src/elements/BaseElement.ts b/packages/module/src/elements/BaseElement.ts index 4f3ae176..8da6b5d3 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,7 @@ export default abstract class BaseElement JSON.stringify(a || {}) === JSON.stringify(b || {}) }) }); } @@ -255,10 +254,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 +267,7 @@ export default abstract class BaseElement n.id === node.getId()); + if (!layoutNode && node.getNodes().length) { + layoutNode = nodes.find(n => n.id === node.getChildren()[0].getId()); } 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..7bd42efe 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/utils/createAggregateEdges.ts b/packages/module/src/utils/createAggregateEdges.ts index 35e7155c..9fe36c04 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 => @@ -27,8 +26,7 @@ const createAggregateEdges = ( ): EdgeModel[] => { 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 +48,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; @@ -88,7 +86,7 @@ const createAggregateEdges = ( return newEdges; }, [] as EdgeModel[] - ); + ) ?? []; }; export { createAggregateEdges }; diff --git a/packages/module/src/utils/element-utils.ts b/packages/module/src/utils/element-utils.ts index 6248d182..9ab934c4 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"