瀏覽代碼

cp system-view to src/plugins

czhen 3 年之前
父節點
當前提交
4be0510eef

+ 78 - 0
client/src/plugins/system/BaseCard.tsx

@@ -0,0 +1,78 @@
+
+import { FC } from 'react';
+import { makeStyles } from '@material-ui/core';
+import { SvgIcon } from '@material-ui/core';
+import { BaseCardProps } from './Types';
+import { ReactComponent } from '../../assets/imgs/pic.svg'
+
+const getStyles = makeStyles(() => ({
+  root: {
+    backgroundColor: 'white',
+    borderRadius: '8px',
+    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+    boxSizing: 'border-box',
+    height: '150px',
+    padding: '16px',
+  },
+  title: {
+    color: '#82838E',
+    fontSize: '14px',
+    marginBottom: '5px',
+    textTransform: 'capitalize',
+  },
+  content: {
+    color: '#010E29',
+    fontSize: '20px',
+    fontWeight: 600,
+    lineHeight: '36px',
+  },
+  desc: {
+    color: '#82838E',
+    fontSize: '14px',
+    lineHeight: '36px',
+    marginLeft: "8px",
+  },
+  emptyRoot: {
+    alignItems: 'center',
+    display: 'flex',
+    flexDirection: 'column',
+    justifyContent: 'flex-start',
+
+    '& > svg': {
+      marginTop: '10px',
+      width: '100%',
+    }
+  },
+  emptyTitle: {
+    fontSize: '14px',
+    marginTop: '14px',
+    textTransform: 'capitalize',
+  },
+  emptyDesc: {
+    fontSize: '10px',
+    color: '#82838E',
+    marginTop: '8px',
+  },
+}));
+
+const BaseCard: FC<BaseCardProps> = (props) => {
+  const classes = getStyles();
+  const { children, title, content, desc } = props;
+  return (
+    <div className={classes.root}>
+      <div className={classes.title}>{title}</div>
+      {content && <span className={classes.content}>{content}</span>}
+      {desc && <span className={classes.desc}>{desc}</span>}
+      {!content && !desc && (
+        <div className={classes.emptyRoot}>
+          <SvgIcon viewBox="0 0 101 26" component={ReactComponent} {...props} />
+          <span className={classes.emptyTitle}>no data available</span>
+          <span className={classes.emptyDesc}>There is no data to show you right now.</span>
+        </div>
+      )}
+      {children}
+    </div>
+  );
+};
+
+export default BaseCard;

+ 195 - 0
client/src/plugins/system/DataCard.tsx

@@ -0,0 +1,195 @@
+
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { makeStyles } from '@material-ui/core';
+import Progress from './Progress';
+import { getByteString } from '../../utils/Format';
+import { DataProgressProps, DataSectionProps, DataCardProps } from './Types';
+
+const getStyles = makeStyles(() => ({
+  root: {
+    backgroundColor: '#F6F6F6',
+    borderTopRightRadius: '8px',
+    borderBottomRightRadius: '8px',
+    height: '100%',
+    padding: '20px 16px',
+    boxSizing: 'border-box',
+  },
+
+  title: {
+    display: 'flex',
+    justifyContent: 'space-between',
+  },
+
+  content: {
+    color: '#010E29',
+    fontSize: '20px',
+    fontWeight: 600,
+    lineHeight: '36px',
+  },
+
+  desc: {
+    color: '#82838E',
+    fontSize: '14px',
+    lineHeight: '36px',
+    marginLeft: "8px",
+  },
+
+  rootName: {
+    color: '#82838E',
+    fontSize: '20px',
+    lineHeight: '24px',
+  },
+
+  childName: {
+    color: '#06AFF2',
+    fontSize: '20px',
+    lineHeight: '24px',
+  },
+
+  ip: {
+    color: '#010E29',
+    fontSize: '16px',
+    lineHeight: '24px',
+  },
+
+  sectionRoot: {
+    borderSpacing: '0 1px',
+    display: 'table',
+    marginTop: '24px',
+    width: '100%'
+  },
+
+  sectionRow: {
+    display: 'table-row',
+  },
+
+  sectionHeaderCell: {
+    display: 'table-cell',
+    color: '#82838E',
+    fontSize: '12px',
+    lineHeight: '24px',
+    padding: '8px 16px',
+    textTransform: 'uppercase',
+    width: '50%',
+  },
+
+  sectionCell: {
+    backgroundColor: 'white',
+    color: '#010E29',
+    display: 'table-cell',
+    fontSize: '14px',
+    lineHeight: '24px',
+    padding: '12px 16px',
+    textTransform: 'capitalize',
+    verticalAlign: 'middle',
+    width: '50%',
+  },
+  progressTitle: {
+    fontSize: '14px',
+    color: '#010E29',
+    lineHeight: '24px',
+    display: 'flex',
+    justifyContent: 'space-between',
+  }
+}));
+
+const DataSection: FC<DataSectionProps> = (props) => {
+  const classes = getStyles();
+  const { titles, contents } = props;
+
+  return (
+    <div className={classes.sectionRoot}>
+      <div className={classes.sectionRow}>
+        {titles.map((titleEntry) => <div key={titleEntry} className={classes.sectionHeaderCell}>{titleEntry}</div>)}
+      </div>
+      {contents.map((contentEntry) => {
+        return (
+          <div key={contentEntry.label} className={classes.sectionRow}>
+            <div className={classes.sectionCell}>
+              {contentEntry.label}
+            </div>
+            <div className={classes.sectionCell}>
+              {contentEntry.value}
+            </div>
+          </div>)
+      })}
+    </div>
+  );
+}
+
+const DataProgress: FC<DataProgressProps> = ({ percent = 0, desc = '' }) => {
+  const classes = getStyles();
+  return (
+    <div>
+      <div className={classes.progressTitle}>
+        <span>{`${Number(percent * 100).toFixed(2)}%`}</span>
+        <span>{desc}</span>
+      </div>
+      <Progress percent={percent} color='#06AFF2' />
+    </div>
+  )
+};
+
+const DataCard: FC<DataCardProps & React.HTMLAttributes<HTMLDivElement>> = (props) => {
+  const classes = getStyles();
+  const { t } = useTranslation('systemView');
+  const { t: commonTrans } = useTranslation();
+  const capacityTrans: { [key in string]: string } = commonTrans('capacity');
+  const { node, extend } = props;
+  const hardwareTitle = [t('hardwareTitle'), t('valueTitle')];
+  const hardwareContent = [];
+  const infos = node?.infos?.hardware_infos || {};
+
+  const {
+    cpu_core_count: cpu = 0,
+    cpu_core_usage: cpuUsage = 0,
+    memory = 1,
+    memory_usage: memoryUsage = 0,
+    disk = 1,
+    disk_usage: diskUsage = 0,
+  } = infos;
+
+  if (extend) {
+    hardwareContent.push({ label: t('thCPUCount'), value: cpu });
+    hardwareContent.push({
+      label: t('thCPUUsage'), value: <DataProgress percent={cpuUsage / 100} />
+    });
+    hardwareContent.push({
+      label: t('thMemUsage'), value: <DataProgress percent={memoryUsage / memory} desc={getByteString(memoryUsage, memory, capacityTrans)} />
+    });
+    hardwareContent.push({
+      label: t('thDiskUsage'), value: <DataProgress percent={diskUsage / disk} desc={getByteString(diskUsage, disk, capacityTrans)} />
+    });
+  }
+
+  const systemTitle = [t('systemTitle'), t('valueTitle')];
+  const systemContent = [];
+  const sysInfos = node?.infos?.system_info || {};
+  const {
+    system_version: version,
+    deploy_mode: mode = '',
+    created_time: create = '',
+    updated_time: update = '',
+  } = sysInfos;
+  systemContent.push({ label: t('thVersion'), value: version });
+  systemContent.push({ label: t('thDeployMode'), value: mode });
+  systemContent.push({ label: t('thCreateTime'), value: create });
+  systemContent.push({ label: t('thUpdateTime'), value: update });
+
+  return (
+    <div className={classes.root}>
+      <div className={classes.title}>
+        <div>
+          <span className={classes.rootName}>Milvus / </span>
+          <span className={classes.childName}>{node?.infos?.name}</span>
+        </div>
+        <div className={classes.ip}>{`${t('thIP')}:${infos?.ip || ''}`}</div>
+      </div>
+      {extend && <DataSection titles={hardwareTitle} contents={hardwareContent} />}
+      <DataSection titles={systemTitle} contents={systemContent} />
+    </div>
+  );
+};
+
+export default DataCard;

+ 125 - 0
client/src/plugins/system/LineChartCard.tsx

@@ -0,0 +1,125 @@
+
+import { FC, useState, useEffect, useRef } from 'react';
+import { makeStyles } from '@material-ui/core';
+import BaseCard from './BaseCard';
+import { LineChartCardProps, LinceChartNode } from './Types';
+
+const getStyles = makeStyles(() => ({
+  root: {
+    transform: 'scaleY(-1)',
+    maxWidth: '90%',
+  },
+  ycoord: {
+    cursor: 'pointer',
+    "&:hover, &:focus": {
+      "& line": {
+        transition: 'all .25s',
+        opacity: 1,
+      },
+    },
+
+    "&:hover": {
+      "& circle": {
+        fill: '#06AFF2',
+      },
+    },
+
+    "&:focus": {
+      outline: 'none',
+
+      "& circle": {
+        fill: '#06F3AF',
+      },
+    },
+  }
+}));
+
+const LineChartCard: FC<LineChartCardProps> = (props) => {
+
+  const FULL_HEIGHT = 60;
+  const FULL_WIDTH = 300;
+  const ROUND = 5;
+  const STEP = 25;
+
+  const classes = getStyles();
+  const { title, value } = props;
+  const [displayNodes, setDisplayNodes] = useState<LinceChartNode[]>([]);
+  const [currentNode, setCurrentNode] = useState<LinceChartNode>({
+    percent: 0,
+    value: 0,
+    timestamp: Date.now(),
+  });
+
+  const max = useRef(1);
+  const isHover = useRef(false);
+  const nodes = useRef<LinceChartNode[]>([]);
+
+  useEffect(() => {
+    // show at most 10 nodes. so remove the earliest node when nodes exceed 10
+    if (nodes.current.length > 9) {
+      nodes.current.shift();
+    }
+
+    if (value && max.current) {
+      // calculate the y-axis max scale
+      let currentMax = max.current;
+      if (value > max.current) {
+        const pow = Math.ceil(Math.log10(value));
+        currentMax = Math.pow(10, pow);
+        max.current = currentMax;
+      }
+
+      // generate a new node and save in ref
+      if (nodes.current) {
+        const newNodes = nodes.current.slice(0);
+        const newNode = {
+          percent: value / currentMax * 100,
+          value,
+          timestamp: Date.now(),
+        }
+        newNodes.push(newNode);
+        nodes.current = newNodes;
+
+        // refresh nodes for display when mouse is not hovering on the chart
+        if (!isHover.current) {
+          setDisplayNodes(newNodes);
+          setCurrentNode(newNode);
+        }
+      }
+    }
+  }, [value]);
+
+  return (
+    nodes.current.length ? (
+      <BaseCard title={title} content={`${Math.round(currentNode.value)}ms`} desc={new Date(currentNode.timestamp).toLocaleString()}>
+        <svg className={classes.root} onMouseEnter={() => isHover.current = true} onMouseLeave={() => isHover.current = false} width={FULL_WIDTH} height={FULL_HEIGHT} viewBox={`0 5 ${FULL_WIDTH} ${FULL_HEIGHT}`} fill="white" xmlns="http://www.w3.org/2000/svg">
+          {
+            displayNodes.map((node, index) => {
+              const x1 = FULL_WIDTH - (displayNodes.length - index + 1) * STEP;
+              const y1 = node.percent * .5 + ROUND * 2;
+
+              let line = null;
+              if (index < displayNodes.length - 1) {
+                const x2 = FULL_WIDTH - (displayNodes.length - index) * STEP;
+                const y2 = displayNodes[index + 1]['percent'] * .5 + ROUND * 2;
+                line = <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#06AFF2" />;
+              }
+              return (
+                <g key={`${node.value}${index}`}>
+                  {line}
+                  <g className={classes.ycoord} onMouseOver={() => { setCurrentNode(node) }}>
+                    <circle cx={x1} cy={y1} r={ROUND} fill="white" stroke="#06AFF2" />
+                    <rect opacity="0" x={x1 - ROUND} y={0} width={ROUND * 2} height={FULL_HEIGHT} fill="#E9E9ED" />
+                    <line opacity="0" x1={x1} y1={0} x2={x1} y2={FULL_WIDTH} strokeWidth="2" stroke="#06AFF2" strokeDasharray="2.5" />
+                  </g>
+                </g>
+              )
+            })
+          }
+        </svg>
+      </BaseCard >
+    ) : <BaseCard title={title} />
+  );
+};
+
+export default LineChartCard;

+ 86 - 0
client/src/plugins/system/MiniTopology.tsx

@@ -0,0 +1,86 @@
+import { FC } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
+import { MiniTopoProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  container: {
+    height: '100%',
+    width: 'auto',
+  },
+  childNode: {
+    transition: 'all .25s',
+    cursor: 'pointer',
+    transformOrigin: '50% 50%',
+    transformBox: 'fill-box',
+
+    '& circle': {
+      transition: 'all .25s',
+    },
+
+    '& text': {
+      transition: 'all .25s',
+    },
+
+    '&:hover, &:focus': {
+      transform: 'scale(1.1)',
+      filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
+    },
+
+    '&:focus': {
+      outline: 'none',
+
+      '& svg path': {
+        fill: 'white',
+      },
+
+      '& circle': {
+        fill: '#06AFF2',
+        stroke: '#06AFF2',
+      },
+
+      '& text': {
+        fill: 'white',
+      }
+    }
+  },
+}));
+
+const capitalize = (s: string) => {
+  return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+const MiniTopo: FC<MiniTopoProps> = (props) => {
+  const classes = getStyles();
+  const { selectedCord, selectedChildNode, setCord } = props;
+
+  const WIDTH = 400;                // width for svg
+  const HEIGHT = 400;               // height for svg
+  const LINE = 80;                // line lenght from lv2 node
+  const ANGLE = 10;                // angle offset for lv2 node
+  const R1 = 45;                    // root node radius
+  const R2 = 30;                    // lv1 node radius
+  const W3 = 20;                    // width of child rect
+
+  const childNodeCenterX = WIDTH / 2 + LINE * Math.cos(ANGLE * Math.PI / 180);
+  const childNodeCenterY = HEIGHT / 2 + LINE * Math.sin(ANGLE * Math.PI / 180);
+
+  return (
+    <svg className={classes.container} width={WIDTH} height={HEIGHT} viewBox={`0 0 ${WIDTH} ${HEIGHT}`} xmlns="http://www.w3.org/2000/svg">
+      <rect width="100%" height="100%" fill="white" />
+      <line x1={`${WIDTH / 3}`} y1={`${HEIGHT / 3}`} x2={childNodeCenterX} y2={childNodeCenterY} stroke="#06AFF2" />
+      <g className={classes.childNode} onClick={() => { setCord(null) }}>
+        <circle cx={`${WIDTH / 3}`} cy={`${HEIGHT / 3}`} r={R1} fill="white" stroke="#06AFF2" />
+        <text fontFamily="Roboto" alignmentBaseline="middle" textAnchor="middle" fill="#06AFF2" fontWeight="700" fontSize="12" x={`${WIDTH / 3}`} y={`${HEIGHT / 3}`}>{selectedCord ? capitalize(selectedCord.infos?.name) : ''}</text>
+      </g>
+      <g>
+        <svg width="60" height="60" viewBox="0 0 60 60" x={childNodeCenterX - 30} y={childNodeCenterY - 30}>
+          <circle cx={R2} cy={R2} r={R2} fill="#06AFF2" stroke="white" />
+          <rect className="selected" x={R2 - W3 / 2} y={R2 - W3 / 2} width={W3} height={W3} fill="white" />
+        </svg>
+        <text fontFamily="Roboto" textAnchor="middle" fill="#82838E" fontSize="12" x={childNodeCenterX} y={childNodeCenterY + 50}>{`${selectedChildNode ? selectedChildNode.infos?.name : ''}`}</text>
+      </g>
+    </svg >
+  );
+};
+
+export default MiniTopo;

+ 151 - 0
client/src/plugins/system/NodeListView.tsx

@@ -0,0 +1,151 @@
+import { FC, useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme } from '@material-ui/core';
+import KeyboardArrowDown from '@material-ui/icons/KeyboardArrowDown';
+import { DataGrid } from '@mui/x-data-grid';
+import { useNavigationHook } from '../../hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+import MiniTopo from './MiniTopology';
+import { getByteString } from '../../utils/Format';
+import DataCard from './DataCard';
+import { NodeListViewProps, Node } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    fontFamily: 'Roboto',
+    margin: '14px 40px',
+    display: 'grid',
+    gridTemplateColumns: 'auto 400px',
+    gridTemplateRows: '40px 400px auto',
+    gridTemplateAreas:
+      `"a a"
+       "b ."
+       "b d"`,
+    height: 'calc(100% - 28px)',
+  },
+  cardContainer: {
+    display: 'grid',
+    gap: '16px',
+    gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))',
+  },
+  contentContainer: {
+    borderRadius: '8px',
+    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+    display: 'grid',
+    marginTop: '14px',
+  },
+  childView: {
+    height: '100%',
+    width: '100%',
+    transition: 'all .25s',
+    position: 'absolute',
+    zIndex: 1000,
+    backgroundColor: 'white',
+  },
+  childCloseBtn: {
+    border: 0,
+    backgroundColor: 'white',
+    gridArea: 'a',
+    cursor: 'pointer',
+    width: '100%',
+  },
+  gridContainer: {
+    gridArea: 'b',
+    display: 'flex',
+  },
+  dataCard: {
+    gridArea: 'd',
+  }
+}));
+
+const NodeListView: FC<NodeListViewProps> = (props) => {
+  useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
+  const { t } = useTranslation('systemView');
+  const { t: commonTrans } = useTranslation();
+  const capacityTrans: { [key in string]: string } = commonTrans('capacity');
+
+  const classes = getStyles();
+  const [selectedChildNode, setSelectedChildNode] = useState<Node | undefined>();
+  const [rows, setRows] = useState<any[]>([]);
+  const { selectedCord, childNodes, setCord } = props;
+
+  let columns: any[] = [
+    {
+      field: 'name',
+      headerName: t('thName'),
+      flex: 1,
+    },
+    {
+      field: 'ip',
+      headerName: t('thIP'),
+      flex: 1,
+    },
+    {
+      field: 'cpuCore',
+      headerName: t('thCPUCount'),
+      flex: 1,
+    },
+    {
+      field: 'cpuUsage',
+      headerName: t('thCPUUsage'),
+      flex: 1,
+    },
+    {
+      field: 'diskUsage',
+      headerName: t('thDiskUsage'),
+      flex: 1,
+    },
+    {
+      field: 'memUsage',
+      headerName: t('thMemUsage'),
+      flex: 1,
+    },
+  ];
+
+  useEffect(() => {
+    if (selectedCord) {
+      const connectedIds = selectedCord.connected.map(node => node.connected_identifier);
+      const newRows: any[] = [];
+      childNodes.forEach(node => {
+        if (connectedIds.includes(node.identifier)) {
+          const dataRow = {
+            id: node?.identifier,
+            ip: node?.infos?.hardware_infos.ip,
+            cpuCore: node?.infos?.hardware_infos.cpu_core_count,
+            cpuUsage: node?.infos?.hardware_infos.cpu_core_usage,
+            diskUsage: getByteString(node?.infos?.hardware_infos.disk_usage, node?.infos?.hardware_infos.disk, capacityTrans),
+            memUsage: getByteString(node?.infos?.hardware_infos.memory_usage, node?.infos?.hardware_infos.memory, capacityTrans),
+            name: node?.infos?.name,
+          }
+          newRows.push(dataRow);
+        }
+      })
+      setRows(newRows);
+    }
+  }, [selectedCord, childNodes, capacityTrans]);
+
+  return (
+    <div className={classes.root}>
+      <button className={classes.childCloseBtn} onClick={() => setCord(null)}>
+        <KeyboardArrowDown />
+      </button>
+      <div className={classes.gridContainer}>
+        <DataGrid
+          rows={rows}
+          columns={columns}
+          hideFooterPagination
+          hideFooterSelectedRowCount
+          onRowClick={(rowData) => {
+            const selectedNode = childNodes.find(node => rowData.row.id === node.identifier);
+            setSelectedChildNode(selectedNode);
+          }}
+        />
+      </div>
+      <MiniTopo selectedCord={selectedCord} setCord={setCord} selectedChildNode={selectedChildNode} />
+      <DataCard className={classes.dataCard} node={selectedChildNode} />
+    </div>
+
+  );
+};
+
+export default NodeListView;

+ 30 - 0
client/src/plugins/system/Progress.tsx

@@ -0,0 +1,30 @@
+
+import { FC } from 'react';
+import { makeStyles } from '@material-ui/core';
+import { ProgressProps } from './Types';
+
+const getStyles = makeStyles(() => ({
+  root: {
+    height: 'auto',
+    transform: 'scaleY(-1)',
+    width: '100%',
+
+    "& line": {
+      transformOrigin: '10px 15px',
+    },
+  },
+}));
+
+const Progress: FC<ProgressProps> = (props) => {
+  const classes = getStyles();
+  const { percent = 0, color = '#06F3AF' } = props;
+
+  return (
+    <svg className={classes.root} width="300" height="30" viewBox="0 0 300 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+      <line x1={10} y1={15} x2={290} y2={15} vectorEffect="non-scaling-stroke" strokeWidth="12" stroke="#AEAEBB" strokeLinecap="round" />
+      <line x1={10} y1={15} x2={290} y2={15} vectorEffect="non-scaling-stroke" transform={`scale(${percent}, 1)`} strokeWidth="12" stroke={color} strokeLinecap="round" />
+    </svg >
+  );
+};
+
+export default Progress;

+ 28 - 0
client/src/plugins/system/ProgressCard.tsx

@@ -0,0 +1,28 @@
+
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import BaseCard from './BaseCard';
+import Progress from './Progress';
+import { getByteString } from '../../utils/Format';
+import { ProgressCardProps } from './Types';
+
+const color1 = '#06F3AF';
+const color2 = '#635DCE';
+
+const ProgressCard: FC<ProgressCardProps> = (props) => {
+  const { title, total, usage } = props;
+  const { t } = useTranslation('systemView');
+  const { t: commonTrans } = useTranslation();
+  const capacityTrans: { [key in string]: string } = commonTrans('capacity');
+
+  const color = title === t('diskTitle') ? color1 : color2;
+  const percent = (usage && total) ? (usage / total) : 0;
+
+  return (
+    <BaseCard title={title} content={`${getByteString(usage, total, capacityTrans)} (${Math.floor(percent * 100)}%)`}>
+      <Progress percent={percent} color={color} />
+    </BaseCard>
+  );
+};
+
+export default ProgressCard;

+ 154 - 0
client/src/plugins/system/SystemView.tsx

@@ -0,0 +1,154 @@
+import { useState, useEffect, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme } from '@material-ui/core';
+import clsx from 'clsx';
+import { useNavigationHook } from '../../hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+import { MilvusHttp } from '../../http/Milvus';
+import { useInterval } from '../../hooks/SystemView';
+import Topo from './Topology';
+import NodeListView from './NodeListView';
+import LineChartCard from './LineChartCard';
+import ProgressCard from './ProgressCard';
+import DataCard from './DataCard';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    fontFamily: 'Roboto',
+    margin: '14px 40px',
+    position: 'relative',
+    height: 'calc(100vh - 80px)',
+    display: 'flex',
+    flexDirection: 'column',
+  },
+  cardContainer: {
+    display: 'grid',
+    gap: '16px',
+    gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))',
+  },
+  transparent: {
+    opacity: 0,
+    transition: 'opacity .5s',
+  },
+  contentContainer: {
+    borderRadius: '8px',
+    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+    display: 'grid',
+    gridTemplateColumns: '1fr auto',
+    marginTop: '14px',
+    height: '100%',
+  },
+  childView: {
+    height: '100%',
+    width: '100%',
+    transition: 'all .25s',
+    position: 'absolute',
+    zIndex: 1000,
+    backgroundColor: 'white',
+  },
+  showChildView: {
+    top: 0,
+    maxHeight: 'auto',
+  },
+  hideChildView: {
+    top: '1000px',
+    maxHeight: 0,
+  },
+  childCloseBtn: {
+    border: 0,
+    backgroundColor: 'white',
+    width: '100%',
+  }
+}));
+
+
+const parseJson = (jsonData: any) => {
+  const nodes: any[] = [];
+  const childNodes: any[] = [];
+
+  const system = {
+    // qps: Math.random() * 1000,
+    letency: Math.random() * 1000,
+    disk: 0,
+    diskUsage: 0,
+    memory: 0,
+    memoryUsage: 0,
+  }
+
+  jsonData?.response?.nodes_info.forEach((node: any) => {
+    const type = node?.infos?.type;
+    // coordinator node
+    if (type.includes("Coord")) {
+      nodes.push(node);
+      // other nodes
+    } else {
+      childNodes.push(node);
+    }
+
+    const info = node.infos.hardware_infos;
+    system.memory += info.memory;
+    system.memoryUsage += info.memory_usage;
+    system.disk += info.disk;
+    system.diskUsage += info.disk_usage;
+  });
+  return { nodes, childNodes, system };
+}
+
+
+const SystemView: any = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
+  const { t } = useTranslation('systemView');
+
+  const classes = getStyles();
+  const INTERVAL = 10000;
+
+  const [data, setData] = useState<{ nodes: any, childNodes: any, system: any }>({ nodes: [], childNodes: [], system: {} });
+  const [selectedNode, setNode] = useState<any>();
+  const [selectedCord, setCord] = useState<any>();
+  const { nodes, childNodes, system } = data;
+
+  useInterval(async () => {
+    if (!selectedCord) {
+      const res = await MilvusHttp.getMetrics();
+      setData(parseJson(res));
+    }
+  }, INTERVAL);
+
+  useEffect(() => {
+    async function fetchData() {
+      const res = await MilvusHttp.getMetrics();
+      setData(parseJson(res));
+    }
+    fetchData();
+  }, []);
+
+  let qps = system?.qps || 0;
+  const letency = system?.letency || 0;
+  const childView = useRef<HTMLInputElement>(null);
+
+  return (
+    <div className={classes.root}>
+      <div className={clsx(classes.cardContainer, selectedCord && classes.transparent)}>
+        <ProgressCard title={t('diskTitle')} usage={system.diskUsage} total={system.disk} />
+        <ProgressCard title={t('memoryTitle')} usage={system.memoryUsage} total={system.memory} />
+        <LineChartCard title={t('qpsTitle')} value={qps} />
+        <LineChartCard title={t('letencyTitle')} value={letency} />
+      </div>
+      <div className={classes.contentContainer}>
+        <Topo nodes={nodes} setNode={setNode} setCord={setCord} />
+        <DataCard node={selectedNode} extend />
+      </div>
+
+      <div
+        ref={childView}
+        className={clsx(classes.childView,
+          selectedCord ? classes.showChildView : classes.hideChildView)}
+      >
+        {selectedCord && (<NodeListView selectedCord={selectedCord} childNodes={childNodes} setCord={setCord} />)}
+      </div>
+    </div >
+
+  );
+};
+
+export default SystemView;

文件差異過大導致無法顯示
+ 178 - 0
client/src/plugins/system/Topology.tsx


+ 71 - 0
client/src/plugins/system/Types.ts

@@ -0,0 +1,71 @@
+import { ReactNode } from "react";
+
+export interface Node {
+  infos: {
+    hardware_infos: any,
+    system_info: any,
+    name: string,
+  },
+  connected: {
+    connected_identifier: number,
+  }[],
+  identifier: number,
+}
+
+export interface ProgressProps {
+  percent: number,
+  color: string,
+}
+
+export interface ProgressCardProps {
+  title: string,
+  total: number,
+  usage: number,
+}
+
+export interface BaseCardProps {
+  children?: ReactNode,
+  title: string,
+  content?: string,
+  desc?: string,
+}
+
+export interface LineChartCardProps {
+  title: string,
+  value: number,
+}
+
+export interface DataProgressProps {
+  percent: number,
+  desc?: string,
+}
+
+export interface DataSectionProps {
+  titles: string[],
+  contents: { label: string, value: string }[],
+}
+
+export interface DataCardProps {
+  node?: Node,
+  extend?: boolean
+}
+
+export interface LinceChartNode {
+  percent: number,
+  value: number,
+  timestamp: number,
+}
+
+type SetCord = (arg1: Node | null) => void;
+
+export interface MiniTopoProps {
+  selectedCord: Node,
+  selectedChildNode: Node | undefined,
+  setCord: SetCord,
+}
+
+export interface NodeListViewProps {
+  selectedCord: Node,
+  childNodes: Node[],
+  setCord: SetCord,
+}

+ 9 - 0
client/src/plugins/system/config.json

@@ -0,0 +1,9 @@
+{
+  "name": "system-view",
+  "version": "0.1.0",
+  "client": {
+    "path": "system",
+    "entry": "SystemView.tsx",
+    "label": "System View"
+  }
+}

部分文件因文件數量過多而無法顯示