mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
fix as path graph layout
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Flex, SkeletonText, Badge, VStack } from '@chakra-ui/react';
|
||||
import ReactFlow from 'react-flow-renderer';
|
||||
import { Background, ReactFlowProvider } from 'react-flow-renderer';
|
||||
import { Handle, Position } from 'react-flow-renderer';
|
||||
import { useConfig, useColorValue, useColorToken, useBreakpointValue } from '~/context';
|
||||
import { useConfig, useColorValue, useColorToken } from '~/context';
|
||||
import { useASNDetail } from '~/hooks';
|
||||
import { Controls } from './controls';
|
||||
import { buildElements } from './util';
|
||||
import { useElements } from './useElements';
|
||||
|
||||
import type { ReactFlowProps } from 'react-flow-renderer';
|
||||
import type { TChart, TNode, TNodeData } from './types';
|
||||
|
||||
export const Chart: React.FC<TChart> = (props: TChart) => {
|
||||
@@ -17,19 +15,17 @@ export const Chart: React.FC<TChart> = (props: TChart) => {
|
||||
|
||||
const dots = useColorToken('colors', 'blackAlpha.500', 'whiteAlpha.400');
|
||||
|
||||
const flowProps = useBreakpointValue<Omit<ReactFlowProps, 'elements'>>({
|
||||
base: { defaultPosition: [0, 300], defaultZoom: 0 },
|
||||
lg: { defaultPosition: [100, 300], defaultZoom: 0.7 },
|
||||
}) ?? { defaultPosition: [100, 300], defaultZoom: 0.7 };
|
||||
|
||||
const elements = useMemo(() => [...buildElements({ asn: primary_asn, name: org_name }, data)], [
|
||||
data,
|
||||
]);
|
||||
const elements = useElements({ asn: primary_asn, name: org_name }, data);
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<Box boxSize="100%" zIndex={1}>
|
||||
<ReactFlow elements={elements} nodeTypes={{ ASNode }} {...flowProps}>
|
||||
<ReactFlow
|
||||
snapToGrid
|
||||
elements={elements}
|
||||
nodeTypes={{ ASNode }}
|
||||
onLoad={inst => setTimeout(() => inst.fitView(), 0)}
|
||||
>
|
||||
<Background color={dots} />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
Modal,
|
||||
Skeleton,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
useDisclosure,
|
||||
Skeleton,
|
||||
ModalCloseButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { useColorValue, useBreakpointValue } from '~/context';
|
||||
|
115
hyperglass/ui/components/path/useElements.ts
Normal file
115
hyperglass/ui/components/path/useElements.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import dagre from 'dagre';
|
||||
import { useMemo } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
import type { FlowElement } from 'react-flow-renderer';
|
||||
import type { BasePath } from './types';
|
||||
|
||||
const NODE_WIDTH = 200;
|
||||
const NODE_HEIGHT = 48;
|
||||
|
||||
export function useElements(base: BasePath, data: TStructuredResponse): FlowElement[] {
|
||||
return useMemo(() => {
|
||||
return [...buildElements(base, data)];
|
||||
}, [data.routes.length]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the positions for each AS Path.
|
||||
* @see https://github.com/MrBlenny/react-flow-chart/issues/61
|
||||
*/
|
||||
function* buildElements(base: BasePath, data: TStructuredResponse): Generator<FlowElement> {
|
||||
const { routes } = data;
|
||||
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
|
||||
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);
|
||||
|
||||
const totalPaths = asPaths.length - 1;
|
||||
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setGraph({ marginx: 20, marginy: 20 });
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
// Set the origin (i.e., the hyperglass user) at the base.
|
||||
g.setNode(base.asn, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||
|
||||
for (const [groupIdx, pathGroup] of asPaths.entries()) {
|
||||
// For each ROUTE's AS Path:
|
||||
|
||||
// Find the route after this one.
|
||||
const nextGroup = groupIdx < totalPaths ? asPaths[groupIdx + 1] : [];
|
||||
|
||||
// Connect the first hop in the AS Path to the base (for dagre).
|
||||
g.setEdge(base.asn, `${groupIdx}-${pathGroup[0]}`);
|
||||
|
||||
// Eliminate duplicate AS Paths.
|
||||
if (!isEqual(pathGroup, nextGroup)) {
|
||||
for (const [idx, asn] of pathGroup.entries()) {
|
||||
// For each ASN in the ROUTE:
|
||||
|
||||
const node = `${groupIdx}-${asn}`;
|
||||
const endIdx = pathGroup.length - 1;
|
||||
|
||||
// Add the AS as a node.
|
||||
g.setNode(node, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||
|
||||
// Connect the first hop in the AS Path to the base (for react-flow).
|
||||
if (idx === 0) {
|
||||
yield {
|
||||
id: `e${base.asn}-${node}`,
|
||||
source: base.asn,
|
||||
target: node,
|
||||
};
|
||||
}
|
||||
// Connect every intermediate hop to each other.
|
||||
if (idx !== endIdx) {
|
||||
const next = `${groupIdx}-${pathGroup[idx + 1]}`;
|
||||
g.setEdge(node, next);
|
||||
yield {
|
||||
id: `e${node}-${next}`,
|
||||
source: node,
|
||||
target: next,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that that nodes are added, create the layout.
|
||||
dagre.layout(g, { rankdir: 'BT', align: 'UR' });
|
||||
|
||||
// Get the base ASN's positions.
|
||||
const x = g.node(base.asn).x - NODE_WIDTH / 2;
|
||||
const y = g.node(base.asn).y + NODE_HEIGHT * 6;
|
||||
|
||||
yield {
|
||||
id: base.asn,
|
||||
type: 'ASNode',
|
||||
position: { x, y },
|
||||
data: { asn: base.asn, name: base.name, hasChildren: true, hasParents: false },
|
||||
};
|
||||
|
||||
for (const [groupIdx, pathGroup] of asPaths.entries()) {
|
||||
const nextGroup = groupIdx < totalPaths ? asPaths[groupIdx + 1] : [];
|
||||
if (!isEqual(pathGroup, nextGroup)) {
|
||||
for (const [idx, asn] of pathGroup.entries()) {
|
||||
const node = `${groupIdx}-${asn}`;
|
||||
const endIdx = pathGroup.length - 1;
|
||||
const x = g.node(node).x - NODE_WIDTH / 2;
|
||||
const y = g.node(node).y - NODE_HEIGHT * (idx * 6);
|
||||
|
||||
// Get each ASN's positions.
|
||||
yield {
|
||||
id: node,
|
||||
type: 'ASNode',
|
||||
position: { x, y },
|
||||
data: {
|
||||
asn,
|
||||
name: `AS${asn}`,
|
||||
hasChildren: idx < endIdx,
|
||||
hasParents: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
import { arrangeIntoTree } from '~/util';
|
||||
|
||||
import type { FlowElement, Elements } from 'react-flow-renderer';
|
||||
import type { PathPart } from '~/types';
|
||||
import type { BasePath } from './types';
|
||||
|
||||
function treeToElement(part: PathPart, len: number, index: number): FlowElement[] {
|
||||
const x = index * 250;
|
||||
const y = -(len * 10);
|
||||
const elements = [
|
||||
{
|
||||
id: String(part.base),
|
||||
type: 'ASNode',
|
||||
position: { x, y },
|
||||
data: {
|
||||
asn: part.base,
|
||||
name: `AS${part.base}`,
|
||||
hasChildren: part.children.length !== 0,
|
||||
hasParents: true,
|
||||
},
|
||||
},
|
||||
] as Elements;
|
||||
|
||||
for (const child of part.children) {
|
||||
let xc = index;
|
||||
if (part.children.length !== 0) {
|
||||
elements.push({
|
||||
id: `e${part.base}-${child.base}`,
|
||||
source: String(part.base),
|
||||
target: String(child.base),
|
||||
});
|
||||
} else {
|
||||
xc = x;
|
||||
}
|
||||
elements.push(...treeToElement(child, part.children.length * 12 + len, xc));
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
export function* buildElements(base: BasePath, data: TStructuredResponse): Generator<FlowElement> {
|
||||
const { routes } = data;
|
||||
// Eliminate empty AS paths & deduplicate non-empty AS paths. Length should be same as count minus empty paths.
|
||||
const asPaths = routes.filter(r => r.as_path.length !== 0).map(r => [...new Set(r.as_path)]);
|
||||
const asTree = arrangeIntoTree(asPaths);
|
||||
const numHops = asPaths.flat().length;
|
||||
const childPaths = asTree.map((a, i) => {
|
||||
return treeToElement(a, asTree.length, i);
|
||||
});
|
||||
|
||||
// Add the first hop at the base.
|
||||
yield {
|
||||
id: base.asn,
|
||||
type: 'ASNode',
|
||||
position: { x: 150, y: numHops * 10 },
|
||||
data: { asn: base.asn, name: base.name, hasChildren: true, hasParents: false },
|
||||
};
|
||||
|
||||
for (const path of childPaths) {
|
||||
// path = Each unique path from origin
|
||||
const first = path[0];
|
||||
yield { id: `e${base.asn}-${first.id}`, source: base.asn, target: first.id };
|
||||
// Add link from base to each first hop.
|
||||
yield { id: `e${base.asn}-${first.id}`, source: base.asn, target: first.id };
|
||||
for (const hop of path) {
|
||||
yield hop;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user