123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- import { makeStyles, Theme, useTheme } from '@material-ui/core';
- import { Dispatch, memo, useContext } from 'react';
- import {
- TOPO_HEIGHT,
- TOPO_LINK_LENGTH,
- TOPO_NODE_R,
- TOPO_WIDTH,
- } from './consts';
- import { getIcon } from './getIcon';
- import { ENodeService, ENodeType, INodeTreeStructure } from './Types';
- import clsx from 'clsx';
- import { formatPrometheusAddress } from '@/utils/Format';
- import { prometheusContext } from '@/context/Prometheus';
- const getStyles = makeStyles((theme: Theme) => ({
- root: {
- borderTopLeftRadius: '8px',
- borderBottomLeftRadius: '8px',
- overflow: 'auto',
- backgroundColor: 'white',
- position: 'relative',
- },
- svg: {
- overflow: 'visible',
- position: 'absolute',
- left: 0,
- top: 0,
- bottom: 0,
- right: 0,
- margin: 'auto',
- },
- prometheusInfoContainer: {
- position: 'absolute',
- display: 'flex',
- fontSize: '12px',
- padding: '4px 8px',
- flexWrap: 'wrap',
- },
- prometheusInfoItem: {
- marginRight: '20px',
- display: 'flex',
- },
- prometheusInfoItemLabel: {
- marginRight: '8px',
- fontWeight: 600,
- color: '#333',
- },
- prometheusInfoItemText: {
- fontWeight: 500,
- color: '#666',
- },
- node: {
- transition: 'all .25s',
- cursor: 'pointer',
- transformOrigin: '50% 50%',
- transformBox: 'fill-box',
- '& circle': {
- transition: 'all .25s',
- },
- '& text': {
- transition: 'all .25s',
- },
- '&:hover': {
- transform: 'scale(1.1)',
- filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
- outline: 'none',
- },
- },
- selected: {
- '& svg path': {
- fill: 'white',
- },
- '& circle': {
- fill: theme.palette.primary.main,
- stroke: theme.palette.primary.main,
- },
- '& text': {
- fill: 'white',
- },
- },
- }));
- const randomList = Array(10)
- .fill(0)
- .map(_ => Math.random());
- const nodesLayout = (
- nodes: INodeTreeStructure[],
- width: number,
- height: number
- ) => {
- const rootNode =
- nodes.find(node => node.service === ENodeService.root) ||
- (nodes.find(node => node.type === ENodeType.coord) as INodeTreeStructure);
- const childrenNodes = nodes.filter(node => node !== rootNode);
- const rootPos = [248, height * 0.45];
- const angleStep = (2 * Math.PI) / Math.max(childrenNodes.length, 3);
- const angleBias = angleStep * 0.4;
- const childrenPos = childrenNodes.map((node, i) => [
- rootPos[0] + Math.cos(angleStep * i) * TOPO_LINK_LENGTH[0],
- rootPos[1] + Math.sin(angleStep * i) * TOPO_LINK_LENGTH[0],
- ]);
- const subChildrenPos = childrenNodes.map((node, i) => {
- const angle = angleStep * i + (randomList[i] - 0.5) * angleBias;
- return [
- rootPos[0] + Math.cos(angle) * TOPO_LINK_LENGTH[1],
- rootPos[1] + Math.sin(angle) * TOPO_LINK_LENGTH[1],
- ];
- });
- return { rootNode, childrenNodes, rootPos, childrenPos, subChildrenPos };
- };
- const Topology = ({
- nodeTree,
- onClick,
- selectedService,
- }: {
- nodeTree: INodeTreeStructure;
- onClick: (service: ENodeService) => void;
- selectedService: ENodeService;
- }) => {
- const width = TOPO_WIDTH;
- const height = TOPO_HEIGHT;
- const classes = getStyles();
- const theme = useTheme();
- const { rootNode, childrenNodes, rootPos, childrenPos, subChildrenPos } =
- nodesLayout(nodeTree.children, width, height);
- const { prometheusAddress, prometheusInstance, prometheusNamespace } =
- useContext(prometheusContext);
- const prometheusInfos = [
- {
- label: 'Prometheus Address',
- value: formatPrometheusAddress(prometheusAddress),
- },
- {
- label: 'Namespace',
- value: prometheusNamespace,
- },
- {
- label: 'Instance',
- value: prometheusInstance,
- },
- ];
- return (
- <div className={classes.root}>
- <div className={classes.prometheusInfoContainer}>
- {prometheusInfos.map(prometheusInfo => (
- <div
- key={prometheusInfo.value}
- className={classes.prometheusInfoItem}
- >
- <div className={classes.prometheusInfoItemLabel}>
- {prometheusInfo.label}:
- </div>
- <div className={classes.prometheusInfoItemText}>
- {prometheusInfo.value}
- </div>
- </div>
- ))}
- </div>
- <svg
- className={classes.svg}
- width={width}
- height={height}
- style={{ overflow: 'visible' }}
- >
- {childrenNodes.map((node, i) => {
- const childPos = childrenPos[i];
- const subChildPos = subChildrenPos[i];
- return (
- <g key={node.label}>
- {node.children.length > 0 && (
- <g
- className={classes.node}
- onClick={() => onClick(node.service)}
- >
- <line
- x1={childPos[0]}
- y1={childPos[1]}
- x2={subChildPos[0]}
- y2={subChildPos[1]}
- stroke={theme.palette.primary.main}
- />
- <circle
- cx={subChildPos[0]}
- cy={subChildPos[1]}
- r={TOPO_NODE_R[2]}
- fill="#fff"
- stroke={theme.palette.primary.main}
- />
- {getIcon(
- node.children[0],
- theme,
- subChildPos[0] - 30,
- subChildPos[1] - 30
- )}
- <text
- fontFamily="Roboto"
- textAnchor="middle"
- fill={theme.palette.attuGrey.dark}
- fontSize="12"
- x={subChildPos[0]}
- y={subChildPos[1] + 50}
- >{`${node.children.length - 1} Node(s)`}</text>
- </g>
- )}
- <g
- className={clsx(
- classes.node,
- node.service === selectedService && classes.selected
- )}
- onClick={() => onClick(node.service)}
- >
- <line
- x1={rootPos[0]}
- y1={rootPos[1]}
- x2={childPos[0]}
- y2={childPos[1]}
- stroke={theme.palette.primary.main}
- />
- <circle
- cx={childPos[0]}
- cy={childPos[1]}
- r={TOPO_NODE_R[1]}
- fill="#fff"
- stroke={theme.palette.primary.main}
- />
- {node.type === ENodeType.overview &&
- getIcon(node, theme, childPos[0] - 12, childPos[1] - 20)}
- <text
- fontFamily="Roboto"
- textAnchor="middle"
- fill={theme.palette.primary.main}
- fontWeight="700"
- fontSize="12"
- x={childPos[0]}
- y={childPos[1] + (node.type === ENodeType.overview ? 18 : 6)}
- >
- {node.type === ENodeType.overview
- ? node.label
- : `${node.type}-${node.label.slice(-5)}`}
- </text>
- </g>
- <g
- onClick={() => onClick(rootNode.service)}
- className={clsx(
- classes.node,
- rootNode.service === selectedService && classes.selected
- )}
- >
- <circle
- cx={rootPos[0]}
- cy={rootPos[1]}
- r={TOPO_NODE_R[0]}
- fill="#fff"
- stroke={theme.palette.primary.main}
- />
- <text
- fontFamily="Roboto"
- textAnchor="middle"
- alignmentBaseline="middle"
- fill={theme.palette.primary.main}
- fontWeight="700"
- fontSize="24"
- x={`${rootPos[0]}`}
- y={`${rootPos[1]}`}
- >
- {rootNode.type === ENodeType.overview
- ? rootNode.label
- : `${rootNode.service}`}
- </text>
- </g>
- </g>
- );
- })}
- </svg>
- </div>
- );
- };
- export default Topology;
|