From 87c4459e271fd391297222a0f4601ef65d731799 Mon Sep 17 00:00:00 2001 From: Aviv Turgeman Date: Thu, 22 Aug 2024 19:19:08 +0300 Subject: [PATCH] Fix topology issues Signed-off-by: Aviv Turgeman --- src/views/states/topology/Topology.tsx | 27 ++++++-- .../components/CustomGroup/CustomGroup.scss | 10 +++ .../{ => CustomGroup}/CustomGroup.tsx | 4 +- .../components/CustomNode/CustomNode.scss | 13 ++++ .../{ => CustomNode}/CustomNode.tsx | 8 ++- src/views/states/topology/utils/constants.ts | 9 ++- src/views/states/topology/utils/factory.ts | 51 +++------------ src/views/states/topology/utils/position.ts | 50 +++++++++++++++ src/views/states/topology/utils/utils.ts | 62 ++++++++++++------- 9 files changed, 158 insertions(+), 76 deletions(-) create mode 100644 src/views/states/topology/components/CustomGroup/CustomGroup.scss rename src/views/states/topology/components/{ => CustomGroup}/CustomGroup.tsx (76%) create mode 100644 src/views/states/topology/components/CustomNode/CustomNode.scss rename src/views/states/topology/components/{ => CustomNode}/CustomNode.tsx (85%) create mode 100644 src/views/states/topology/utils/position.ts diff --git a/src/views/states/topology/Topology.tsx b/src/views/states/topology/Topology.tsx index ff92fbc8..5bce1748 100644 --- a/src/views/states/topology/Topology.tsx +++ b/src/views/states/topology/Topology.tsx @@ -17,13 +17,14 @@ import { V1beta1NodeNetworkState } from '@types'; import TopologySidebar from './components/TopologySidebar/TopologySidebar'; import TopologyToolbar from './components/TopologyToolbar/TopologyToolbar'; +import { GRAPH_POSITIONING_EVENT, NODE_POSITIONING_EVENT } from './utils/constants'; import { componentFactory, layoutFactory } from './utils/factory'; +import { restoreNodePositions, saveNodePositions } from './utils/position'; import { transformDataToTopologyModel } from './utils/utils'; const Topology: FC = () => { const [selectedIds, setSelectedIds] = useState([]); const [visualization, setVisualization] = useState(null); - const [selectedNodeFilters, setSelectedNodeFilters] = useState([]); const [states, loaded, error] = useK8sWatchResource({ @@ -39,12 +40,12 @@ const Topology: FC = () => { useEffect(() => { if (loaded && !error) { - const topologyModel = + const filteredStates = selectedNodeFilters.length > 0 - ? transformDataToTopologyModel( - states.filter((state) => selectedNodeFilters.includes(state.metadata.name)), - ) - : transformDataToTopologyModel(states); + ? states.filter((state) => selectedNodeFilters.includes(state.metadata.name)) + : undefined; + + const topologyModel = transformDataToTopologyModel(states, filteredStates); if (!visualization) { const newVisualization = new Visualization(); @@ -53,13 +54,27 @@ const Topology: FC = () => { newVisualization.addEventListener(SELECTION_EVENT, setSelectedIds); newVisualization.setFitToScreenOnLayout(true); newVisualization.fromModel(topologyModel); + restoreNodePositions(newVisualization); + setVisualization(newVisualization); } else { visualization.fromModel(topologyModel); + restoreNodePositions(visualization); } } }, [states, loaded, error, selectedNodeFilters]); + useEffect(() => { + if (visualization) { + visualization.addEventListener(NODE_POSITIONING_EVENT, () => + saveNodePositions(visualization), + ); + visualization.addEventListener(GRAPH_POSITIONING_EVENT, () => + saveNodePositions(visualization), + ); + } + }, [visualization]); + return ( text { + fill: var(--pf-topology__node__label__text--Fill); + } + + .pf-topology__node__label__background { + fill: var(--pf-topology__node__label__background--Fill); + stroke: var(--pf-topology__node__background--Stroke); + } +} diff --git a/src/views/states/topology/components/CustomGroup.tsx b/src/views/states/topology/components/CustomGroup/CustomGroup.tsx similarity index 76% rename from src/views/states/topology/components/CustomGroup.tsx rename to src/views/states/topology/components/CustomGroup/CustomGroup.tsx index 35540949..bc72f8db 100644 --- a/src/views/states/topology/components/CustomGroup.tsx +++ b/src/views/states/topology/components/CustomGroup/CustomGroup.tsx @@ -8,6 +8,8 @@ import { WithSelectionProps, } from '@patternfly/react-topology'; +import './CustomGroup.scss'; + type CustomGroupProps = { element: Node; } & WithSelectionProps & @@ -17,7 +19,7 @@ type CustomGroupProps = { const CustomGroup: FC = ({ element, ...rest }) => { const data = element.getData(); - return ; + return ; }; export default CustomGroup; diff --git a/src/views/states/topology/components/CustomNode/CustomNode.scss b/src/views/states/topology/components/CustomNode/CustomNode.scss new file mode 100644 index 00000000..27bebc8d --- /dev/null +++ b/src/views/states/topology/components/CustomNode/CustomNode.scss @@ -0,0 +1,13 @@ +.custom-node { + .pf-topology__node__background { + stroke-width: 4px; + } + + .pf-topology__node__label__background { + stroke-width: 2px; + } +} + +.custom-node.pf-topology__node.pf-m-selected .pf-topology__node__background { + stroke-width: 4px; +} diff --git a/src/views/states/topology/components/CustomNode.tsx b/src/views/states/topology/components/CustomNode/CustomNode.tsx similarity index 85% rename from src/views/states/topology/components/CustomNode.tsx rename to src/views/states/topology/components/CustomNode/CustomNode.tsx index 54d3922e..eab8b2bb 100644 --- a/src/views/states/topology/components/CustomNode.tsx +++ b/src/views/states/topology/components/CustomNode/CustomNode.tsx @@ -8,7 +8,9 @@ import { WithSelectionProps, } from '@patternfly/react-topology'; -import { ICON_SIZE } from '../utils/constants'; +import { ICON_SIZE } from '../../utils/constants'; + +import './CustomNode.scss'; type CustomNodeProps = { element: Node; @@ -16,7 +18,7 @@ type CustomNodeProps = { WithDragNodeProps & WithDndDropProps; -const CustomNode: FC = ({ element, onSelect, selected }) => { +const CustomNode: FC = ({ element, onSelect, selected, ...rest }) => { const data = element.getData(); const Icon = data.icon; const { width, height } = element.getBounds(); @@ -26,11 +28,13 @@ const CustomNode: FC = ({ element, onSelect, selected }) => { return ( diff --git a/src/views/states/topology/utils/constants.ts b/src/views/states/topology/utils/constants.ts index b3175e2c..6daf6ff0 100644 --- a/src/views/states/topology/utils/constants.ts +++ b/src/views/states/topology/utils/constants.ts @@ -1,4 +1,9 @@ -export const NODE_DIAMETER = 35; +export const NODE_DIAMETER = 70; +export const ICON_SIZE = 30; + export const CONNECTOR_TARGET_DROP = 'connector-target-drop'; export const GROUP = 'group'; -export const ICON_SIZE = 15; + +export const TOPOLOGY_LOCAL_STORAGE_KEY = 'topologyNodePositions'; +export const NODE_POSITIONING_EVENT = 'node-positioned'; +export const GRAPH_POSITIONING_EVENT = 'graph-position-change'; diff --git a/src/views/states/topology/utils/factory.ts b/src/views/states/topology/utils/factory.ts index 9bf1bdd2..932a776f 100644 --- a/src/views/states/topology/utils/factory.ts +++ b/src/views/states/topology/utils/factory.ts @@ -2,29 +2,21 @@ import { ColaLayout, ComponentFactory, DefaultEdge, - DragObjectWithType, - Edge, Graph, GraphComponent, - GraphElement, - groupDropTargetSpec, Layout, LayoutFactory, ModelKind, - Node, nodeDragSourceSpec, - nodeDropTargetSpec, - withDndDrop, withDragNode, withPanZoom, withSelection, - withTargetDrag, } from '@patternfly/react-topology'; -import CustomGroup from '../components/CustomGroup'; -import CustomNode from '../components/CustomNode'; +import CustomGroup from '../components/CustomGroup/CustomGroup'; +import CustomNode from '../components/CustomNode/CustomNode'; -import { CONNECTOR_TARGET_DROP, GROUP } from './constants'; +import { GROUP } from './constants'; export const layoutFactory: LayoutFactory = (type: string, graph: Graph): Layout | undefined => new ColaLayout(graph, { layoutOnDrag: false }); @@ -32,46 +24,17 @@ export const layoutFactory: LayoutFactory = (type: string, graph: Graph): Layout export const componentFactory: ComponentFactory = (kind: ModelKind, type: string) => { switch (type) { case GROUP: - return withDndDrop(groupDropTargetSpec)( - withDragNode(nodeDragSourceSpec(GROUP))(withSelection()(CustomGroup)), - ); + return withDragNode(nodeDragSourceSpec(GROUP))(withSelection()(CustomGroup)); default: switch (kind) { case ModelKind.graph: return withPanZoom()(GraphComponent); case ModelKind.node: - return withDndDrop(nodeDropTargetSpec([CONNECTOR_TARGET_DROP]))( - withDragNode(nodeDragSourceSpec(ModelKind.node, true, true))( - withSelection()(CustomNode), - ), + return withDragNode(nodeDragSourceSpec(ModelKind.node, true, true))( + withSelection()(CustomNode), ); case ModelKind.edge: - return withTargetDrag< - DragObjectWithType, - Node, - { dragging?: boolean }, - { - element: GraphElement; - } - >({ - item: { type: CONNECTOR_TARGET_DROP }, - begin: (monitor, props) => { - props.element.raise(); - return props.element; - }, - drag: (event, monitor, props) => { - (props.element as Edge).setEndPoint(event.x, event.y); - }, - end: (dropResult, monitor, props) => { - if (monitor.didDrop() && dropResult && props) { - (props.element as Edge).setTarget(dropResult); - } - (props.element as Edge).setEndPoint(); - }, - collect: (monitor) => ({ - dragging: monitor.isDragging(), - }), - })(DefaultEdge); + return DefaultEdge; default: return undefined; } diff --git a/src/views/states/topology/utils/position.ts b/src/views/states/topology/utils/position.ts new file mode 100644 index 00000000..303f7bdc --- /dev/null +++ b/src/views/states/topology/utils/position.ts @@ -0,0 +1,50 @@ +import { Point, Visualization } from '@patternfly/react-topology'; + +import { TOPOLOGY_LOCAL_STORAGE_KEY } from './constants'; + +export const saveNodePositions = (visualization: Visualization) => { + const graph = visualization.getGraph(); + const nodePositions = {}; + + // Traverse all nodes and their children + graph.getNodes().forEach((node) => { + if (node.isGroup()) { + // Save the group node position + nodePositions[node.getId()] = node.getPosition(); + + // Save all child node positions + node.getAllNodeChildren().forEach((childNode) => { + nodePositions[childNode.getId()] = childNode.getPosition(); + }); + } else { + nodePositions[node.getId()] = node.getPosition(); + } + }); + + localStorage.setItem(TOPOLOGY_LOCAL_STORAGE_KEY, JSON.stringify(nodePositions)); +}; + +export const restoreNodePositions = (visualization: Visualization) => { + const savedPositions = localStorage.getItem(TOPOLOGY_LOCAL_STORAGE_KEY); + if (savedPositions) { + const nodePositions = JSON.parse(savedPositions); + const graph = visualization.getGraph(); + + // Traverse all nodes and their children + graph.getNodes().forEach((node) => { + if (nodePositions[node.getId()]) { + node.setPosition(new Point(nodePositions[node.getId()].x, nodePositions[node.getId()].y)); + } + + if (node.isGroup()) { + node.getAllNodeChildren().forEach((childNode) => { + if (nodePositions[childNode.getId()]) { + childNode.setPosition( + new Point(nodePositions[childNode.getId()].x, nodePositions[childNode.getId()].y), + ); + } + }); + } + }); + } +}; diff --git a/src/views/states/topology/utils/utils.ts b/src/views/states/topology/utils/utils.ts index 8be6e096..435747c7 100644 --- a/src/views/states/topology/utils/utils.ts +++ b/src/views/states/topology/utils/utils.ts @@ -1,4 +1,4 @@ -import { NetworkIcon } from '@patternfly/react-icons'; +import { EthernetIcon, LinkIcon, NetworkIcon } from '@patternfly/react-icons'; import { EdgeModel, Model, @@ -7,7 +7,7 @@ import { NodeShape, NodeStatus, } from '@patternfly/react-topology'; -import { NodeNetworkConfigurationInterface, V1beta1NodeNetworkState } from '@types'; +import { InterfaceType, NodeNetworkConfigurationInterface, V1beta1NodeNetworkState } from '@types'; import { GROUP, NODE_DIAMETER } from './constants'; @@ -17,29 +17,38 @@ const statusMap: { [key: string]: NodeStatus } = { absent: NodeStatus.warning, }; -export const getStatus = (iface: NodeNetworkConfigurationInterface): NodeStatus => { +const getStatus = (iface: NodeNetworkConfigurationInterface): NodeStatus => { return statusMap[iface.state.toLowerCase()] || NodeStatus.default; }; +const getIcon = (iface: NodeNetworkConfigurationInterface) => { + if (iface.ethernet || iface.type === InterfaceType.ETHERNET) return EthernetIcon; + if (iface.type === InterfaceType.BOND) return LinkIcon; + return NetworkIcon; +}; + const createNodes = ( nnsName: string, interfaces: NodeNetworkConfigurationInterface[], ): NodeModel[] => { - return interfaces.map((iface) => ({ - id: `${nnsName}~${iface.name}`, - type: ModelKind.node, - label: iface.name, - width: NODE_DIAMETER, - height: NODE_DIAMETER, - visible: !iface.patch, - shape: NodeShape.ellipse, - status: getStatus(iface), - data: { - badge: 'I', - icon: NetworkIcon, - }, - parent: nnsName, - })); + return interfaces.map((iface) => { + const icon = getIcon(iface); + return { + id: `${nnsName}~${iface.name}`, + type: ModelKind.node, + label: iface.name, + width: NODE_DIAMETER, + height: NODE_DIAMETER, + visible: !iface.patch && iface.type !== InterfaceType.LOOPBACK, + shape: NodeShape.circle, + status: getStatus(iface), + data: { + badge: 'I', + icon, + }, + parent: nnsName, + }; + }); }; const createEdges = ( @@ -47,7 +56,7 @@ const createEdges = ( interfaces: NodeNetworkConfigurationInterface[], ): EdgeModel[] => { const edges: EdgeModel[] = []; - const patchConnections: { [key: string]: string } = {}; // Track patch connections + const patchConnections: { [key: string]: string } = {}; interfaces.forEach((iface: NodeNetworkConfigurationInterface) => { if (iface.patch?.peer) { @@ -112,13 +121,14 @@ const createEdges = ( return edges; }; -const createGroupNode = (nnsName: string, childNodeIds: string[]): NodeModel => { +const createGroupNode = (nnsName: string, childNodeIds: string[], visible: boolean): NodeModel => { return { id: nnsName, type: GROUP, label: nnsName, group: true, children: childNodeIds, + visible, style: { padding: 40, }, @@ -128,7 +138,10 @@ const createGroupNode = (nnsName: string, childNodeIds: string[]): NodeModel => }; }; -export const transformDataToTopologyModel = (data: V1beta1NodeNetworkState[]): Model => { +export const transformDataToTopologyModel = ( + data: V1beta1NodeNetworkState[], + filteredData?: V1beta1NodeNetworkState[], // Optional filtered data parameter +): Model => { const nodes: NodeModel[] = []; const edges: EdgeModel[] = []; @@ -136,9 +149,16 @@ export const transformDataToTopologyModel = (data: V1beta1NodeNetworkState[]): M const nnsName = nodeState.metadata.name; const childNodes = createNodes(nnsName, nodeState.status.currentState.interfaces); + + // Determine visibility based on whether filteredData includes this nodeState + const isVisible = filteredData + ? filteredData.some((filteredState) => filteredState.metadata.name === nnsName) + : true; + const groupNode = createGroupNode( nnsName, childNodes.map((child) => child.id), + isVisible, ); const nodeEdges = createEdges(nnsName, nodeState.status.currentState.interfaces);