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

Fix topology issues #109

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions src/views/states/topology/Topology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>([]);
const [visualization, setVisualization] = useState<Visualization>(null);

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

const [states, loaded, error] = useK8sWatchResource<V1beta1NodeNetworkState[]>({
Expand All @@ -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();
Expand All @@ -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 (
<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
50 changes: 50 additions & 0 deletions src/views/states/topology/utils/position.ts
Original file line number Diff line number Diff line change
@@ -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),
);
}
});
}
});
}
};
Loading
Loading