2
0

Topology.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import { makeStyles, Theme, useTheme } from '@material-ui/core';
  2. import { Dispatch, memo, useContext } from 'react';
  3. import {
  4. TOPO_HEIGHT,
  5. TOPO_LINK_LENGTH,
  6. TOPO_NODE_R,
  7. TOPO_WIDTH,
  8. } from './consts';
  9. import { getIcon } from './getIcon';
  10. import { ENodeService, ENodeType, INodeTreeStructure } from './Types';
  11. import clsx from 'clsx';
  12. import { formatPrometheusAddress } from '@/utils/Format';
  13. import { prometheusContext } from '@/context/Prometheus';
  14. const getStyles = makeStyles((theme: Theme) => ({
  15. root: {
  16. borderTopLeftRadius: '8px',
  17. borderBottomLeftRadius: '8px',
  18. overflow: 'auto',
  19. backgroundColor: 'white',
  20. position: 'relative',
  21. },
  22. svg: {
  23. overflow: 'visible',
  24. position: 'absolute',
  25. left: 0,
  26. top: 0,
  27. bottom: 0,
  28. right: 0,
  29. margin: 'auto',
  30. },
  31. prometheusInfoContainer: {
  32. position: 'absolute',
  33. display: 'flex',
  34. fontSize: '12px',
  35. padding: '4px 8px',
  36. flexWrap: 'wrap',
  37. },
  38. prometheusInfoItem: {
  39. marginRight: '20px',
  40. display: 'flex',
  41. },
  42. prometheusInfoItemLabel: {
  43. marginRight: '8px',
  44. fontWeight: 600,
  45. color: '#333',
  46. },
  47. prometheusInfoItemText: {
  48. fontWeight: 500,
  49. color: '#666',
  50. },
  51. node: {
  52. transition: 'all .25s',
  53. cursor: 'pointer',
  54. transformOrigin: '50% 50%',
  55. transformBox: 'fill-box',
  56. '& circle': {
  57. transition: 'all .25s',
  58. },
  59. '& text': {
  60. transition: 'all .25s',
  61. },
  62. '&:hover': {
  63. transform: 'scale(1.1)',
  64. filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
  65. outline: 'none',
  66. },
  67. },
  68. selected: {
  69. '& svg path': {
  70. fill: 'white',
  71. },
  72. '& circle': {
  73. fill: theme.palette.primary.main,
  74. stroke: theme.palette.primary.main,
  75. },
  76. '& text': {
  77. fill: 'white',
  78. },
  79. },
  80. }));
  81. const randomList = Array(10)
  82. .fill(0)
  83. .map(_ => Math.random());
  84. const nodesLayout = (
  85. nodes: INodeTreeStructure[],
  86. width: number,
  87. height: number
  88. ) => {
  89. const rootNode =
  90. nodes.find(node => node.service === ENodeService.root) ||
  91. (nodes.find(node => node.type === ENodeType.coord) as INodeTreeStructure);
  92. const childrenNodes = nodes.filter(node => node !== rootNode);
  93. const rootPos = [248, height * 0.45];
  94. const angleStep = (2 * Math.PI) / Math.max(childrenNodes.length, 3);
  95. const angleBias = angleStep * 0.4;
  96. const childrenPos = childrenNodes.map((node, i) => [
  97. rootPos[0] + Math.cos(angleStep * i) * TOPO_LINK_LENGTH[0],
  98. rootPos[1] + Math.sin(angleStep * i) * TOPO_LINK_LENGTH[0],
  99. ]);
  100. const subChildrenPos = childrenNodes.map((node, i) => {
  101. const angle = angleStep * i + (randomList[i] - 0.5) * angleBias;
  102. return [
  103. rootPos[0] + Math.cos(angle) * TOPO_LINK_LENGTH[1],
  104. rootPos[1] + Math.sin(angle) * TOPO_LINK_LENGTH[1],
  105. ];
  106. });
  107. return { rootNode, childrenNodes, rootPos, childrenPos, subChildrenPos };
  108. };
  109. const Topology = ({
  110. nodeTree,
  111. onClick,
  112. selectedService,
  113. }: {
  114. nodeTree: INodeTreeStructure;
  115. onClick: (service: ENodeService) => void;
  116. selectedService: ENodeService;
  117. }) => {
  118. const width = TOPO_WIDTH;
  119. const height = TOPO_HEIGHT;
  120. const classes = getStyles();
  121. const theme = useTheme();
  122. const { rootNode, childrenNodes, rootPos, childrenPos, subChildrenPos } =
  123. nodesLayout(nodeTree.children, width, height);
  124. const { prometheusAddress, prometheusInstance, prometheusNamespace } =
  125. useContext(prometheusContext);
  126. const prometheusInfos = [
  127. {
  128. label: 'Prometheus Address',
  129. value: formatPrometheusAddress(prometheusAddress),
  130. },
  131. {
  132. label: 'Namespace',
  133. value: prometheusNamespace,
  134. },
  135. {
  136. label: 'Instance',
  137. value: prometheusInstance,
  138. },
  139. ];
  140. return (
  141. <div className={classes.root}>
  142. <div className={classes.prometheusInfoContainer}>
  143. {prometheusInfos.map(prometheusInfo => (
  144. <div
  145. key={prometheusInfo.value}
  146. className={classes.prometheusInfoItem}
  147. >
  148. <div className={classes.prometheusInfoItemLabel}>
  149. {prometheusInfo.label}:
  150. </div>
  151. <div className={classes.prometheusInfoItemText}>
  152. {prometheusInfo.value}
  153. </div>
  154. </div>
  155. ))}
  156. </div>
  157. <svg
  158. className={classes.svg}
  159. width={width}
  160. height={height}
  161. style={{ overflow: 'visible' }}
  162. >
  163. {childrenNodes.map((node, i) => {
  164. const childPos = childrenPos[i];
  165. const subChildPos = subChildrenPos[i];
  166. return (
  167. <g key={node.label}>
  168. {node.children.length > 0 && (
  169. <g
  170. className={classes.node}
  171. onClick={() => onClick(node.service)}
  172. >
  173. <line
  174. x1={childPos[0]}
  175. y1={childPos[1]}
  176. x2={subChildPos[0]}
  177. y2={subChildPos[1]}
  178. stroke={theme.palette.primary.main}
  179. />
  180. <circle
  181. cx={subChildPos[0]}
  182. cy={subChildPos[1]}
  183. r={TOPO_NODE_R[2]}
  184. fill="#fff"
  185. stroke={theme.palette.primary.main}
  186. />
  187. {getIcon(
  188. node.children[0],
  189. theme,
  190. subChildPos[0] - 30,
  191. subChildPos[1] - 30
  192. )}
  193. <text
  194. fontFamily="Roboto"
  195. textAnchor="middle"
  196. fill={theme.palette.attuGrey.dark}
  197. fontSize="12"
  198. x={subChildPos[0]}
  199. y={subChildPos[1] + 50}
  200. >{`${node.children.length - 1} Node(s)`}</text>
  201. </g>
  202. )}
  203. <g
  204. className={clsx(
  205. classes.node,
  206. node.service === selectedService && classes.selected
  207. )}
  208. onClick={() => onClick(node.service)}
  209. >
  210. <line
  211. x1={rootPos[0]}
  212. y1={rootPos[1]}
  213. x2={childPos[0]}
  214. y2={childPos[1]}
  215. stroke={theme.palette.primary.main}
  216. />
  217. <circle
  218. cx={childPos[0]}
  219. cy={childPos[1]}
  220. r={TOPO_NODE_R[1]}
  221. fill="#fff"
  222. stroke={theme.palette.primary.main}
  223. />
  224. {node.type === ENodeType.overview &&
  225. getIcon(node, theme, childPos[0] - 12, childPos[1] - 20)}
  226. <text
  227. fontFamily="Roboto"
  228. textAnchor="middle"
  229. fill={theme.palette.primary.main}
  230. fontWeight="700"
  231. fontSize="12"
  232. x={childPos[0]}
  233. y={childPos[1] + (node.type === ENodeType.overview ? 18 : 6)}
  234. >
  235. {node.type === ENodeType.overview
  236. ? node.label
  237. : `${node.type}-${node.label.slice(-5)}`}
  238. </text>
  239. </g>
  240. <g
  241. onClick={() => onClick(rootNode.service)}
  242. className={clsx(
  243. classes.node,
  244. rootNode.service === selectedService && classes.selected
  245. )}
  246. >
  247. <circle
  248. cx={rootPos[0]}
  249. cy={rootPos[1]}
  250. r={TOPO_NODE_R[0]}
  251. fill="#fff"
  252. stroke={theme.palette.primary.main}
  253. />
  254. <text
  255. fontFamily="Roboto"
  256. textAnchor="middle"
  257. alignmentBaseline="middle"
  258. fill={theme.palette.primary.main}
  259. fontWeight="700"
  260. fontSize="24"
  261. x={`${rootPos[0]}`}
  262. y={`${rootPos[1]}`}
  263. >
  264. {rootNode.type === ENodeType.overview
  265. ? rootNode.label
  266. : `${rootNode.service}`}
  267. </text>
  268. </g>
  269. </g>
  270. );
  271. })}
  272. </svg>
  273. </div>
  274. );
  275. };
  276. export default Topology;