Skip to content
This repository has been archived by the owner on Nov 28, 2024. It is now read-only.

Commit

Permalink
OCPBUGS-42859: Interface with same name causes unexpected behavior in…
Browse files Browse the repository at this point in the history
… NNS topology

Signed-off-by: Aviv Turgeman <[email protected]>
  • Loading branch information
avivtur committed Oct 9, 2024
1 parent 237a2a9 commit 8c44c1f
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 159 deletions.
132 changes: 63 additions & 69 deletions src/views/states/topology/Topology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
VisualizationSurface,
} from '@patternfly/react-topology';
import { V1beta1NodeNetworkState } from '@types';
import { isEmpty } from '@utils/helpers';

import TopologySidebar from './components/TopologySidebar/TopologySidebar';
import TopologyToolbar from './components/TopologyToolbar/TopologyToolbar';
Expand All @@ -39,84 +40,77 @@ const Topology: FC = () => {
);

useEffect(() => {
if (loaded && !error) {
const filteredStates =
selectedNodeFilters.length > 0
? states.filter((state) => selectedNodeFilters.includes(state.metadata.name))
: undefined;
if (!loaded || error || isEmpty(states)) return;

const topologyModel = transformDataToTopologyModel(states, filteredStates);
const filteredStates =
selectedNodeFilters.length > 0
? states.filter((state) => selectedNodeFilters.includes(state.metadata.name))
: undefined;

if (!visualization) {
const newVisualization = new Visualization();
newVisualization.registerLayoutFactory(layoutFactory);
newVisualization.registerComponentFactory(componentFactory);
newVisualization.addEventListener(SELECTION_EVENT, setSelectedIds);
newVisualization.setFitToScreenOnLayout(true);
newVisualization.fromModel(topologyModel);
restoreNodePositions(newVisualization);
const topologyModel = transformDataToTopologyModel(states, filteredStates);

setVisualization(newVisualization);
} else {
visualization.fromModel(topologyModel);
restoreNodePositions(visualization);
}
}
}, [states, loaded, error, selectedNodeFilters]);

useEffect(() => {
if (visualization) {
visualization.addEventListener(NODE_POSITIONING_EVENT, () =>
saveNodePositions(visualization),
if (!visualization) {
const newVisualization = new Visualization();
newVisualization.registerLayoutFactory(layoutFactory);
newVisualization.registerComponentFactory(componentFactory);
newVisualization.addEventListener(SELECTION_EVENT, setSelectedIds);
newVisualization.addEventListener(NODE_POSITIONING_EVENT, () =>
saveNodePositions(newVisualization),
);
visualization.addEventListener(GRAPH_POSITIONING_EVENT, () =>
saveNodePositions(visualization),
newVisualization.addEventListener(GRAPH_POSITIONING_EVENT, () =>
saveNodePositions(newVisualization),
);
newVisualization.setFitToScreenOnLayout(true);
newVisualization.fromModel(topologyModel, false);
restoreNodePositions(newVisualization);
setVisualization(newVisualization);
} else {
visualization.fromModel(topologyModel);
}
}, [visualization]);
}, [states, loaded, error, selectedNodeFilters]);

return (
<TopologyView
sideBar={
<TopologySidebar
states={states}
selectedIds={selectedIds}
setSelectedIds={setSelectedIds}
/>
}
viewToolbar={
<TopologyToolbar
nodeNames={nodeNames}
selectedNodeFilters={selectedNodeFilters}
setSelectedNodeFilters={setSelectedNodeFilters}
/>
}
controlBar={
<TopologyControlBar
controlButtons={createTopologyControlButtons({
...defaultControlButtonsOptions,
zoomInCallback: action(() => {
visualization.getGraph().scaleBy(4 / 3);
}),
zoomOutCallback: action(() => {
visualization.getGraph().scaleBy(0.75);
}),
fitToScreenCallback: action(() => {
visualization.getGraph().fit(40);
}),
resetViewCallback: action(() => {
visualization.getGraph().reset();
visualization.getGraph().layout();
}),
legend: false,
})}
/>
}
>
<VisualizationProvider controller={visualization}>
<VisualizationProvider controller={visualization}>
<TopologyView
sideBar={
<TopologySidebar
states={states}
selectedIds={selectedIds}
setSelectedIds={setSelectedIds}
/>
}
viewToolbar={
<TopologyToolbar
nodeNames={nodeNames}
selectedNodeFilters={selectedNodeFilters}
setSelectedNodeFilters={setSelectedNodeFilters}
/>
}
controlBar={
<TopologyControlBar
controlButtons={createTopologyControlButtons({
...defaultControlButtonsOptions,
zoomInCallback: action(() => {
visualization.getGraph().scaleBy(4 / 3);
}),
zoomOutCallback: action(() => {
visualization.getGraph().scaleBy(0.75);
}),
fitToScreenCallback: action(() => {
visualization.getGraph().fit(40);
}),
resetViewCallback: action(() => {
visualization.getGraph().reset();
visualization.getGraph().layout();
}),
legend: false,
})}
/>
}
>
<VisualizationSurface state={{ selectedIds }} />
</VisualizationProvider>
</TopologyView>
</TopologyView>
</VisualizationProvider>
);
};

Expand Down
13 changes: 2 additions & 11 deletions src/views/states/topology/components/CustomNode/CustomNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,15 @@ type CustomNodeProps = {
WithDragNodeProps &
WithDndDropProps;

const CustomNode: FC<CustomNodeProps> = ({ element, onSelect, selected, ...rest }) => {
const CustomNode: FC<CustomNodeProps> = ({ element, ...rest }) => {
const data = element.getData();
const Icon = data.icon;
const { width, height } = element.getBounds();

const xCenter = (width - ICON_SIZE) / 2;
const yCenter = (height - ICON_SIZE) / 2;

return (
<DefaultNode
className="custom-node"
badge={data.badge}
element={element}
onSelect={onSelect}
selected={selected}
truncateLength={8}
{...rest}
>
<DefaultNode className="custom-node" element={element} truncateLength={8} {...rest}>
<g transform={`translate(${xCenter}, ${yCenter})`}>
<Icon width={ICON_SIZE} height={ICON_SIZE} />
</g>
Expand Down
7 changes: 1 addition & 6 deletions src/views/states/topology/utils/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,11 @@ export const saveNodePositions = (visualization: Visualization) => {

// Traverse all nodes and their children
graph.getNodes().forEach((node) => {
nodePositions[node.getId()] = node.getPosition();
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();
}
});

Expand Down
142 changes: 69 additions & 73 deletions src/views/states/topology/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ const getStatus = (iface: NodeNetworkConfigurationInterface): NodeStatus => {
};

const getIcon = (iface: NodeNetworkConfigurationInterface) => {
if (!isEmpty(iface.bridge)) return BridgeIcon;
if (
iface.bridge ||
iface.type === InterfaceType.OVS_BRIDGE ||
iface.type === InterfaceType.LINUX_BRIDGE
)
return BridgeIcon;
if (iface.ethernet || iface.type === InterfaceType.ETHERNET) return EthernetIcon;
if (iface.type === InterfaceType.BOND) return LinkIcon;
return NetworkIcon;
Expand All @@ -34,92 +39,83 @@ const getIcon = (iface: NodeNetworkConfigurationInterface) => {
const createNodes = (
nnsName: string,
interfaces: NodeNetworkConfigurationInterface[],
): NodeModel[] => {
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,
};
});
};
): NodeModel[] =>
interfaces.map((iface) => ({
id: `${nnsName}~${iface.name}~${iface.type}`,
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: {
icon: getIcon(iface),
type: iface.type,
bridgePorts: iface.bridge?.port,
bondPorts: iface['link-aggregation']?.port,
vlanBaseInterface: iface.vlan?.['base-iface'],
},
parent: nnsName,
}));

const createEdges = (
nnsName: string,
interfaces: NodeNetworkConfigurationInterface[],
): EdgeModel[] => {
const createEdges = (childNodes: NodeModel[]): EdgeModel[] => {
const edges: EdgeModel[] = [];
const patchConnections: { [key: string]: string } = {};

interfaces.forEach((iface: NodeNetworkConfigurationInterface) => {
if (iface.patch?.peer) {
patchConnections[iface.name] = iface.patch.peer;
}
});
childNodes.forEach((sourceNode) => {
// Find bridge connections
if (!isEmpty(sourceNode.data?.bridgePorts)) {
sourceNode.data?.bridgePorts.forEach((port) => {
const targetNode = childNodes.find(
(target) => target.label === port.name && target.id !== sourceNode.id,
);

interfaces.forEach((iface: NodeNetworkConfigurationInterface) => {
const nodeId = `${nnsName}~${iface.name}`;

if (iface.bridge?.port) {
iface.bridge.port.forEach((prt) => {
if (patchConnections[prt.name]) {
const peerPatch = patchConnections[prt.name];
const peerBridge = interfaces.find((intf) =>
intf.bridge?.port.some((p) => p.name === peerPatch),
);

if (peerBridge) {
const peerBridgeId = `${nnsName}~${peerBridge.name}`;
edges.push({
id: `${nodeId}~${peerBridgeId}-edge`,
type: ModelKind.edge,
source: nodeId,
target: peerBridgeId,
});
}
} else if (prt.name && iface.name !== prt.name) {
if (!isEmpty(targetNode)) {
edges.push({
id: `${nodeId}~${prt.name}-edge`,
id: `${sourceNode.id}~${targetNode.id}-edge`,
type: ModelKind.edge,
source: nodeId,
target: `${nnsName}~${prt.name}`,
source: sourceNode.id,
target: targetNode.id,
});
}
});
}

if (iface.vlan?.['base-iface'] && iface.name === iface.vlan?.['base-iface']) {
edges.push({
id: `${nodeId}~${iface.vlan['base-iface']}-edge`,
type: ModelKind.edge,
source: nodeId,
target: `${nnsName}~${iface.vlan['base-iface']}`,
});
}
// Find bond connections
if (!isEmpty(sourceNode.data?.vlanBaseInterface)) {
sourceNode.data?.bondPorts.forEach((port) => {
const targetNode = childNodes.find(
(target) => target.label === port && target.id !== sourceNode.id,
);

if (iface['link-aggregation']?.port) {
iface['link-aggregation'].port.forEach((prt: string) => {
if (iface.name !== prt) {
if (!isEmpty(targetNode)) {
edges.push({
id: `${nodeId}~${prt}-edge`,
id: `${sourceNode.id}~${targetNode.id}-edge`,
type: ModelKind.edge,
source: nodeId,
target: `${nnsName}~${prt}`,
source: sourceNode.id,
target: targetNode.id,
});
}
});
}

// Find vlan connections
if (!isEmpty(sourceNode.data?.bondPorts)) {
const baseInterface = sourceNode.data?.vlanBaseInterface;

const targetNode = childNodes.find(
(target) => target.label === baseInterface && target.id !== sourceNode.id,
);

if (!isEmpty(targetNode)) {
edges.push({
id: `${sourceNode.id}~${targetNode.id}-edge`,
type: ModelKind.edge,
source: sourceNode.id,
target: targetNode.id,
});
}
}
});

return edges;
Expand All @@ -144,7 +140,7 @@ const createGroupNode = (nnsName: string, childNodeIds: string[], visible: boole

export const transformDataToTopologyModel = (
data: V1beta1NodeNetworkState[],
filteredData?: V1beta1NodeNetworkState[], // Optional filtered data parameter
filteredData?: V1beta1NodeNetworkState[],
): Model => {
const nodes: NodeModel[] = [];
const edges: EdgeModel[] = [];
Expand All @@ -154,7 +150,6 @@ export const transformDataToTopologyModel = (

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;
Expand All @@ -164,9 +159,10 @@ export const transformDataToTopologyModel = (
childNodes.map((child) => child.id),
isVisible,
);
const nodeEdges = createEdges(nnsName, nodeState.status.currentState.interfaces);

nodes.push(groupNode, ...childNodes);

const nodeEdges = createEdges(childNodes);

edges.push(...nodeEdges);
});

Expand Down

0 comments on commit 8c44c1f

Please sign in to comment.