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

Commit

Permalink
Fix topology issues
Browse files Browse the repository at this point in the history
Signed-off-by: Aviv Turgeman <[email protected]>
  • Loading branch information
avivtur committed Aug 26, 2024
1 parent 5c2212b commit c9a18b4
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 69 deletions.
18 changes: 17 additions & 1 deletion src/views/states/topology/Topology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TopologyView,
Visualization,
VisualizationProvider,
Point,

Check failure on line 14 in src/views/states/topology/Topology.tsx

View workflow job for this annotation

GitHub Actions / Run linter and tests

'Point' is defined but never used
VisualizationSurface,
} from '@patternfly/react-topology';
import { V1beta1NodeNetworkState } from '@types';
Expand All @@ -19,11 +20,12 @@ import TopologySidebar from './components/TopologySidebar/TopologySidebar';
import TopologyToolbar from './components/TopologyToolbar/TopologyToolbar';
import { componentFactory, layoutFactory } from './utils/factory';
import { transformDataToTopologyModel } from './utils/utils';
import { restoreNodePositions, saveNodePositions } from './utils/position';
import { GRAPH_POSITIONING_EVENT, NODE_POSITIONING_EVENT } from './utils/constants';

const Topology: FC = () => {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [visualization, setVisualization] = useState<Visualization>(null);

const [selectedNodeFilters, setSelectedNodeFilters] = useState<string[]>([]);

const [states, loaded, error] = useK8sWatchResource<V1beta1NodeNetworkState[]>({
Expand Down Expand Up @@ -53,13 +55,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 (
<TopologyView
sideBar={
Expand Down
10 changes: 10 additions & 0 deletions src/views/states/topology/components/CustomGroup/CustomGroup.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.custom-group {
.pf-topology__group__label > 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
WithSelectionProps,
} from '@patternfly/react-topology';

import './CustomGroup.scss';

type CustomGroupProps = {
element: Node;
} & WithSelectionProps &
Expand All @@ -17,7 +19,7 @@ type CustomGroupProps = {
const CustomGroup: FC<CustomGroupProps> = ({ element, ...rest }) => {
const data = element.getData();

return <DefaultGroup badge={data?.badge} element={element} {...rest} />;
return <DefaultGroup className="custom-group" badge={data?.badge} element={element} {...rest} />;
};

export default CustomGroup;
13 changes: 13 additions & 0 deletions src/views/states/topology/components/CustomNode/CustomNode.scss
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ 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;
} & WithSelectionProps &
WithDragNodeProps &
WithDndDropProps;

const CustomNode: FC<CustomNodeProps> = ({ element, onSelect, selected }) => {
const CustomNode: FC<CustomNodeProps> = ({ element, onSelect, selected, ...rest }) => {
const data = element.getData();
const Icon = data.icon;
const { width, height } = element.getBounds();
Expand All @@ -26,11 +28,13 @@ const CustomNode: FC<CustomNodeProps> = ({ element, onSelect, selected }) => {

return (
<DefaultNode
className="custom-node"
badge={data.badge}
element={element}
onSelect={onSelect}
selected={selected}
truncateLength={8}
{...rest}
>
<g transform={`translate(${xCenter}, ${yCenter})`}>
<Icon width={ICON_SIZE} height={ICON_SIZE} />
Expand Down
9 changes: 7 additions & 2 deletions src/views/states/topology/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
51 changes: 7 additions & 44 deletions src/views/states/topology/utils/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,39 @@ 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 });

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;
}
Expand Down
49 changes: 49 additions & 0 deletions src/views/states/topology/utils/position.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Visualization, Point } from '@patternfly/react-topology';

Check warning on line 1 in src/views/states/topology/utils/position.ts

View workflow job for this annotation

GitHub Actions / Run linter and tests

Run autofix to sort these imports!
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),
);
}
});
}
});
}
};
47 changes: 28 additions & 19 deletions src/views/states/topology/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NetworkIcon } from '@patternfly/react-icons';
import { EthernetIcon, LinkIcon, NetworkIcon } from '@patternfly/react-icons';
import {
EdgeModel,
Model,
Expand All @@ -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';

Expand All @@ -17,37 +17,46 @@ 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 = (
nnsName: string,
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) {
Expand Down

0 comments on commit c9a18b4

Please sign in to comment.