Browse Source

Merge pull request #239 from sutcalag/main

refine systemview
ryjiang 3 years ago
parent
commit
00cb012a73

+ 1 - 0
client/package.json

@@ -9,6 +9,7 @@
     "@material-ui/core": "^4.11.4",
     "@material-ui/core": "^4.11.4",
     "@material-ui/icons": "^4.11.2",
     "@material-ui/icons": "^4.11.2",
     "@material-ui/lab": "^4.0.0-alpha.58",
     "@material-ui/lab": "^4.0.0-alpha.58",
+    "@mui/x-data-grid": "^4.0.0",
     "@testing-library/jest-dom": "^5.11.4",
     "@testing-library/jest-dom": "^5.11.4",
     "@testing-library/react": "^11.1.0",
     "@testing-library/react": "^11.1.0",
     "@testing-library/user-event": "^12.1.10",
     "@testing-library/user-event": "^12.1.10",

+ 5 - 0
client/src/assets/imgs/pic.svg

@@ -0,0 +1,5 @@
+<svg width="101" height="26" viewBox="0 0 101 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M42.3543 1.34849C42.2296 0.941106 41.7918 0.910427 41.6336 1.29801L35.1123 17.2803C34.6944 18.3045 33.5818 18.3875 33.0751 17.4323L29.3395 10.39C29.1783 10.0861 28.8286 10.0943 28.6761 10.4057L24.79 18.3423C24.5718 18.7881 24.1916 19.0583 23.7829 19.0583H0V18.034H23.7829C23.9191 18.034 24.0459 17.9439 24.1186 17.7954L28.0047 9.85872C28.4621 8.92467 29.5113 8.89982 29.9949 9.81156L33.7305 16.8539C33.8994 17.1723 34.2702 17.1446 34.4096 16.8032L40.9309 0.820936C41.4053 -0.341793 42.7189 -0.249776 43.093 0.972398L48.2151 17.7099C48.2749 17.9055 48.4214 18.034 48.5844 18.034H54V19.0583H48.5844C48.0953 19.0583 47.656 18.6728 47.4764 18.086L42.3543 1.34849Z" fill="#AEAEBB"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M93.167 11.3491C93.0008 10.9756 92.4919 10.9449 92.2837 11.2959L88.5732 17.551C88.0307 18.4656 86.7582 18.545 86.1102 17.7047L84.2853 15.3383C84.0802 15.0723 83.6824 15.0801 83.4875 15.3539L81.2998 18.4273C81.0176 18.8237 80.5661 19.0583 80.0856 19.0583H64V18.0392H80.0856C80.2458 18.0392 80.3963 17.961 80.4903 17.8288L82.6781 14.7554C83.2628 13.934 84.4562 13.9106 85.0716 14.7085L86.8965 17.0749C87.1125 17.355 87.5366 17.3286 87.7174 17.0237L91.428 10.7686L91.8559 11.0322L91.428 10.7686C92.0525 9.71579 93.5792 9.80776 94.0777 10.9282L97.1083 17.74C97.1894 17.9222 97.3674 18.0392 97.5637 18.0392H101V19.0583H97.5637C96.975 19.0583 96.4407 18.7073 96.1976 18.1609L93.167 11.3491Z" fill="#AEAEBB"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.2971 8.70321L62.7051 7.81494L63.6133 8.23464L63.2054 9.12291L62.2971 8.70321ZM60.6653 12.2563L61.4812 10.4797L62.3895 10.8994L61.5736 12.676L60.6653 12.2563ZM59.0335 15.8094L59.8494 14.0328L60.7576 14.4525L59.9417 16.2291L59.0335 15.8094ZM57.4017 19.3624L58.2176 17.5859L59.1258 18.0056L58.3099 19.7821L57.4017 19.3624ZM55.7698 22.9155L56.5857 21.139L57.494 21.5587L56.6781 23.3352L55.7698 22.9155ZM54.546 25.5803L54.9539 24.692L55.8622 25.1117L55.4542 26L54.546 25.5803Z" fill="#AEAEBB"/>
+</svg>

+ 20 - 0
client/src/hooks/SystemView.tsx

@@ -0,0 +1,20 @@
+
+import { useRef, useEffect } from 'react';
+
+export const useInterval = (callback: Function, delay: number) => {
+  const savedCallback = useRef() as { current: any };
+
+  useEffect(() => {
+    savedCallback.current = callback;
+  });
+
+  useEffect(() => {
+    function tick() {
+      savedCallback.current();
+    }
+    if (delay) {
+      let id = setInterval(tick, delay);
+      return () => clearInterval(id);
+    }
+  }, [delay]);
+}

+ 8 - 0
client/src/http/Milvus.ts

@@ -5,6 +5,7 @@ export class MilvusHttp extends BaseModel {
   static CONNECT_URL = '/milvus/connect';
   static CONNECT_URL = '/milvus/connect';
   static CHECK_URL = '/milvus/check';
   static CHECK_URL = '/milvus/check';
   static FLUSH_URL = '/milvus/flush';
   static FLUSH_URL = '/milvus/flush';
+  static METRICS_URL = '/milvus/metrics';
   static TIGGER_CRON_URL = '/crons';
   static TIGGER_CRON_URL = '/crons';
 
 
   constructor(props: {}) {
   constructor(props: {}) {
@@ -29,6 +30,13 @@ export class MilvusHttp extends BaseModel {
     });
     });
   }
   }
 
 
+  static getMetrics() {
+    return super.search({
+      path: this.METRICS_URL,
+      params: {},
+    });
+  }
+
   static triggerCron(data: { name: WS_EVENTS; type: WS_EVENTS_TYPE }) {
   static triggerCron(data: { name: WS_EVENTS; type: WS_EVENTS_TYPE }) {
     return super.update({
     return super.update({
       path: this.TIGGER_CRON_URL,
       path: this.TIGGER_CRON_URL,

+ 21 - 0
client/src/i18n/cn/systemView.ts

@@ -0,0 +1,21 @@
+const systemViewTrans = {
+  diskTitle: 'disk',
+  memoryTitle: 'memory',
+  qpsTitle: 'qps',
+  letencyTitle: 'letency',
+  hardwareTitle: 'hardware',
+  valueTitle: 'value',
+  systemTitle: 'system',
+  thName: 'Node Name',
+  thIP: 'IP',
+  thCPUCount: 'CPU Core Count',
+  thCPUUsage: 'CUP Core Usage',
+  thDiskUsage: 'Disk Usage',
+  thMemUsage: 'Memory Usage',
+  thVersion: 'version',
+  thDeployMode: 'deploy mode',
+  thCreateTime: 'create time',
+  thUpdateTime: 'updated time',
+};
+
+export default systemViewTrans;

+ 8 - 0
client/src/i18n/en/common.ts

@@ -38,6 +38,14 @@ const commonTrans = {
     join: 'Join our growing social community today',
     join: 'Join our growing social community today',
     get: 'Get insight, tips and share ideas',
     get: 'Get insight, tips and share ideas',
   },
   },
+  capacity: {
+    b: 'B',
+    kb: 'KB',
+    mb: 'MB',
+    gb: 'GB',
+    tb: 'TB',
+    pb: 'PB',
+  },
 };
 };
 
 
 export default commonTrans;
 export default commonTrans;

+ 21 - 0
client/src/i18n/en/systemView.ts

@@ -0,0 +1,21 @@
+const systemViewTrans = {
+  diskTitle: 'disk',
+  memoryTitle: 'memory',
+  qpsTitle: 'qps',
+  letencyTitle: 'letency',
+  hardwareTitle: 'hardware',
+  valueTitle: 'value',
+  systemTitle: 'system',
+  thName: 'Node Name',
+  thIP: 'IP',
+  thCPUCount: 'CPU Core Count',
+  thCPUUsage: 'CUP Core Usage',
+  thDiskUsage: 'Disk Usage',
+  thMemUsage: 'Memory Usage',
+  thVersion: 'version',
+  thDeployMode: 'deploy mode',
+  thCreateTime: 'create time',
+  thUpdateTime: 'updated time',
+};
+
+export default systemViewTrans;

+ 4 - 0
client/src/i18n/index.ts

@@ -25,6 +25,8 @@ import insertEn from './en/insert';
 import insertCn from './cn/insert';
 import insertCn from './cn/insert';
 import searchEn from './en/search';
 import searchEn from './en/search';
 import searchCn from './cn/search';
 import searchCn from './cn/search';
+import systemViewTransEn from './en/systemView';
+import systemViewTransCn from './cn/systemView';
 
 
 export const resources = {
 export const resources = {
   cn: {
   cn: {
@@ -40,6 +42,7 @@ export const resources = {
     index: indexCn,
     index: indexCn,
     insert: insertCn,
     insert: insertCn,
     search: searchCn,
     search: searchCn,
+    systemView: systemViewTransCn,
   },
   },
   en: {
   en: {
     translation: commonEn,
     translation: commonEn,
@@ -54,6 +57,7 @@ export const resources = {
     index: indexEn,
     index: indexEn,
     insert: insertEn,
     insert: insertEn,
     search: searchEn,
     search: searchEn,
+    systemView: systemViewTransEn
   },
   },
 };
 };
 
 

+ 78 - 0
client/src/pages/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/pages/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;

+ 0 - 110
client/src/pages/system/LineChart.tsx

@@ -1,110 +0,0 @@
-
-import { makeStyles, Theme } from '@material-ui/core';
-
-const getStyles = makeStyles((theme: Theme) => ({
-  root: {
-    transform: 'scaleY(-1)',
-  },
-  ycoord: {
-    cursor: 'pointer',
-
-    "& circle": {
-      transition: 'all .25s',
-    },
-
-    "&:hover, &:focus": {
-      "& line": {
-        transition: 'all .25s',
-        opacity: 1,
-      },
-    },
-
-    "&:hover": {
-      "& circle": {
-        fill: '#06AFF2',
-      },
-    },
-
-    "&:focus": {
-      outline: 'none',
-
-      "& circle": {
-        fill: '#06F3AF',
-      },
-    },
-  }
-}));
-
-const LineChart = (props: any) => {
-  const fullHeight = 100;
-  const fullWidth = 300;
-  const step = 30;
-  const classes = getStyles();
-  // const { nodes } = props;
-  const nodes = [
-    {
-      percent: 90,
-      value: 2000,
-      timestamp: 1629947929204,
-    },
-
-    {
-      percent: 30,
-      value: 2000,
-      timestamp: 1629947329204,
-    },
-
-    {
-      percent: 50,
-      value: 2000,
-      timestamp: 1629947129204,
-    },
-
-    {
-      percent: 80,
-      value: 2000,
-      timestamp: 1629947129204,
-    },
-
-    {
-      percent: 30,
-      value: 2000,
-      timestamp: 1629947129204,
-    },
-
-    {
-      percent: 20,
-      value: 2000,
-      timestamp: 1629947129204,
-    },
-  ];
-
-  return (
-    <svg className={classes.root} width="300" height="100" viewBox="0 0 300 100" fill="none" xmlns="http://www.w3.org/2000/svg">
-      {
-        nodes.map((node, index) => {
-          const x1 = fullWidth - (nodes.length - index + 1) * step;
-          const y1 = node.percent;
-          let line = null;
-          if (index < nodes.length - 1) {
-            const x2 = fullWidth - (nodes.length - index) * step;
-            const y2 = nodes[index + 1]['percent'];
-            line = <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#06AFF2" />;
-          }
-          return (
-            <g>
-              {line}
-              <g className={classes.ycoord} tabIndex={0}>
-                <circle cx={x1} cy={y1} r={3} fill="white" stroke="#06AFF2" />
-                <rect opacity="0" x={x1 - 5} y={0} width="10" height={fullHeight} fill="#E9E9ED" />
-                <line opacity="0" x1={x1} y1={0} x2={x1} y2={fullWidth} stroke="#06AFF2" stroke-dasharray="2.5" />
-              </g>
-            </g>
-          )
-        })
-      }
-    </svg >
-  );
-};
-
-export default LineChart;

+ 125 - 0
client/src/pages/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/pages/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/pages/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;

+ 13 - 20
client/src/pages/system/Progress.tsx

@@ -1,35 +1,28 @@
 
 
-import { makeStyles, Theme } from '@material-ui/core';
+import { FC } from 'react';
+import { makeStyles } from '@material-ui/core';
+import { ProgressProps } from './Types';
 
 
-const getStyles = makeStyles((theme: Theme) => ({
+const getStyles = makeStyles(() => ({
   root: {
   root: {
+    height: 'auto',
     transform: 'scaleY(-1)',
     transform: 'scaleY(-1)',
-  },
-  ycoord: {
-    cursor: 'pointer',
-
-    "&:hover": {
-      "& line": {
-        transition: 'all .25s',
-        opacity: 1,
-      },
+    width: '100%',
 
 
-      "& circle": {
-        transition: 'all .25s',
-        fill: '#06AFF2',
-      },
+    "& line": {
+      transformOrigin: '10px 15px',
     },
     },
-  }
+  },
 }));
 }));
 
 
-const Progress = (props: any) => {
+const Progress: FC<ProgressProps> = (props) => {
   const classes = getStyles();
   const classes = getStyles();
   const { percent = 0, color = '#06F3AF' } = props;
   const { percent = 0, color = '#06F3AF' } = props;
 
 
   return (
   return (
-    <svg className={classes.root} width="300" height="100" viewBox="0 0 300 100" fill="none" xmlns="http://www.w3.org/2000/svg">
-      <line x1={10} y1={50} x2={250} y2={50} stroke-width="12" stroke="#AEAEBB" stroke-linecap="round" />
-      <line x1={10} y1={50} x2={250 * percent / 100} y2={50} stroke-width="12" stroke={color} stroke-linecap="round" />
+    <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 >
     </svg >
   );
   );
 };
 };

+ 28 - 0
client/src/pages/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;

+ 142 - 9
client/src/pages/system/SystemView.tsx

@@ -1,20 +1,153 @@
+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 { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
+import { MilvusHttp } from '../../http/Milvus';
+import { useInterval } from '../../hooks/SystemView';
 import Topo from './Topology';
 import Topo from './Topology';
-import LineChart from './LineChart';
-import Progress from './Progress';
+import NodeListView from './NodeListView';
+import LineChartCard from './LineChartCard';
+import ProgressCard from './ProgressCard';
+import DataCard from './DataCard';
 
 
-const SystemView = () => {
+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);
   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 (
   return (
-    <div>
+    <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 >
 
 
-      <Progress percent={50} color={"#06F3AF"} />
-      <Progress percent={90} color={"#635DCE"} />
-      <LineChart />
-      <Topo />
-    </div>
   );
   );
 };
 };
 
 

File diff suppressed because it is too large
+ 44 - 58
client/src/pages/system/Topology.tsx


+ 71 - 0
client/src/pages/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,
+}

+ 31 - 0
client/src/utils/Format.ts

@@ -142,3 +142,34 @@ export const getCreateFieldType = (config: Field): CreateFieldType => {
 
 
 // Trim the address
 // Trim the address
 export const formatAddress = (address: string): string => address.trim();
 export const formatAddress = (address: string): string => address.trim();
+
+// generate a sting like 20.22/98.33MB with proper unit
+export const getByteString = (value1: number, value2: number, capacityTrans: { [key in string]: string }) => {
+  if (!value1 || !value2) return `0${capacityTrans.b}`;
+  const power = Math.round(Math.log(value1) / Math.log(1024));
+  let unit = '';
+  switch (power) {
+    case 1:
+      unit = capacityTrans.kb;
+      break;
+    case 2:
+      unit = capacityTrans.mb;
+      break;
+    case 3:
+      unit = capacityTrans.gb;
+      break;
+    case 4:
+      unit = capacityTrans.tb;
+      break;
+    case 5:
+      unit = capacityTrans.pb;
+      break;
+    default:
+      unit = capacityTrans.b;
+      break;
+  }
+  const byteValue1 = value1 / (1024 ** power);
+  const byteValue2 = value2 / (1024 ** power);
+
+  return `${(byteValue1).toFixed(2)}/${(byteValue2).toFixed(2)} ${unit}`;
+}

+ 47 - 2
client/yarn.lock

@@ -1165,6 +1165,13 @@
   dependencies:
   dependencies:
     regenerator-runtime "^0.13.4"
     regenerator-runtime "^0.13.4"
 
 
+"@babel/runtime@^7.14.8":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
+  integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3":
 "@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3":
   version "7.12.13"
   version "7.12.13"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
@@ -1540,6 +1547,27 @@
     prop-types "^15.7.2"
     prop-types "^15.7.2"
     react-is "^16.8.0 || ^17.0.0"
     react-is "^16.8.0 || ^17.0.0"
 
 
+"@material-ui/utils@^5.0.0-beta.4":
+  version "5.0.0-beta.5"
+  resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-5.0.0-beta.5.tgz#de492037e1f1f0910fda32e6f11b66dfcde2a1c2"
+  integrity sha512-wtJ3ovXWZdTAz5eLBqvMpYH/IBJb3qMQbGCyL1i00+sf7AUlAuv4QLx+QtX/siA6L7IpxUQVfqpoCpQH1eYRpQ==
+  dependencies:
+    "@babel/runtime" "^7.14.8"
+    "@types/prop-types" "^15.7.4"
+    "@types/react-is" "^16.7.1 || ^17.0.0"
+    prop-types "^15.7.2"
+    react-is "^17.0.2"
+
+"@mui/x-data-grid@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-4.0.0.tgz#e9e9c33a8b86e85872c48f30f4a8de72ee819153"
+  integrity sha512-BYn7uLx5tJbMarcWltjjVArWNBdC22/2xOpq3Azhltbb3TRx3h2RLUeKwZI685xmGHmzvtu6QqaoYqQgFe/h+g==
+  dependencies:
+    "@material-ui/utils" "^5.0.0-beta.4"
+    clsx "^1.1.1"
+    prop-types "^15.7.2"
+    reselect "^4.0.0"
+
 "@nodelib/fs.scandir@2.1.5":
 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
   version "2.1.5"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1965,6 +1993,11 @@
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
   integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
   integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
 
 
+"@types/prop-types@^15.7.4":
+  version "15.7.4"
+  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
+  integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
+
 "@types/q@^1.5.1":
 "@types/q@^1.5.1":
   version "1.5.4"
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
@@ -1991,6 +2024,13 @@
   dependencies:
   dependencies:
     "@types/react" "*"
     "@types/react" "*"
 
 
+"@types/react-is@^16.7.1 || ^17.0.0":
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.2.tgz#abc4d910bff5b0bc6b3e1bec57575f6b63fd4e05"
+  integrity sha512-2+L0ilcAEG8udkDnvx8B0upwXFBbNnVwOsSCTxW3SDOkmar9NyEeLG0ZLa3uOEw9zyYf/fQapcnfXAVmDKlyHw==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-router-dom@^5.1.7":
 "@types/react-router-dom@^5.1.7":
   version "5.1.7"
   version "5.1.7"
   resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271"
   resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271"
@@ -3544,7 +3584,7 @@ cliui@^6.0.0:
     strip-ansi "^6.0.0"
     strip-ansi "^6.0.0"
     wrap-ansi "^6.2.0"
     wrap-ansi "^6.2.0"
 
 
-clsx@^1.0.4:
+clsx@^1.0.4, clsx@^1.1.1:
   version "1.1.1"
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
   integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
   integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -9580,7 +9620,7 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
 
-"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1:
+"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
   version "17.0.2"
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
   integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
   integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@@ -9942,6 +9982,11 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
 
+reselect@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
+  integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+
 resolve-cwd@^2.0.0:
 resolve-cwd@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"

Some files were not shown because too many files changed in this diff