Browse Source

Merge pull request #168 from alwayslove2013/system_healthy

[Attu Metrics] Healthy Status View
ryjiang 2 năm trước cách đây
mục cha
commit
7f5a9d6759
44 tập tin đã thay đổi với 3080 bổ sung18 xóa
  1. 4 2
      client/package.json
  2. 4 0
      client/public/env-config.js
  3. 10 7
      client/src/App.tsx
  4. 9 2
      client/src/components/customRadio/CustomRadio.tsx
  5. 9 0
      client/src/consts/Localstorage.ts
  6. 1 0
      client/src/consts/Milvus.tsx
  7. 13 0
      client/src/consts/Prometheus.tsx
  8. 116 0
      client/src/context/Prometheus.tsx
  9. 12 0
      client/src/context/Types.ts
  10. 6 3
      client/src/http/BaseModel.ts
  11. 42 0
      client/src/http/Prometheus.ts
  12. 4 0
      client/src/i18n/cn/common.ts
  13. 10 0
      client/src/i18n/cn/prometheus.ts
  14. 4 0
      client/src/i18n/en/common.ts
  15. 10 0
      client/src/i18n/en/prometheus.ts
  16. 4 0
      client/src/i18n/index.ts
  17. 69 1
      client/src/pages/connect/AuthForm.tsx
  18. 6 1
      client/src/pages/index.tsx
  19. 203 0
      client/src/pages/systemHealthy/HealthyIndexDetailView.tsx
  20. 58 0
      client/src/pages/systemHealthy/HealthyIndexLegend.tsx
  21. 141 0
      client/src/pages/systemHealthy/HealthyIndexOverview.tsx
  22. 32 0
      client/src/pages/systemHealthy/HealthyIndexRow.tsx
  23. 95 0
      client/src/pages/systemHealthy/LineChartLarge.tsx
  24. 125 0
      client/src/pages/systemHealthy/LineChartSmall.tsx
  25. 174 0
      client/src/pages/systemHealthy/SystemHealthyView.tsx
  26. 177 0
      client/src/pages/systemHealthy/ThresholdSetting.tsx
  27. 59 0
      client/src/pages/systemHealthy/TimeRangeTabs.tsx
  28. 296 0
      client/src/pages/systemHealthy/Topology.tsx
  29. 79 0
      client/src/pages/systemHealthy/Types.ts
  30. 48 0
      client/src/pages/systemHealthy/consts.ts
  31. 154 0
      client/src/pages/systemHealthy/dataHandler.ts
  32. 15 0
      client/src/pages/systemHealthy/getIcon.tsx
  33. 198 0
      client/src/pages/systemHealthy/prometheusDataCase.json
  34. 5 0
      client/src/router/Router.tsx
  35. 13 1
      client/src/utils/Format.ts
  36. 487 1
      client/yarn.lock
  37. 1 0
      server/package.json
  38. 2 0
      server/src/app.ts
  39. 0 0
      server/src/prometheus/dto.ts
  40. 34 0
      server/src/prometheus/fillRangeData.ts
  41. 6 0
      server/src/prometheus/index.ts
  42. 46 0
      server/src/prometheus/prometheus.controller.ts
  43. 280 0
      server/src/prometheus/prometheus.service.ts
  44. 19 0
      server/yarn.lock

+ 4 - 2
client/package.json

@@ -14,6 +14,7 @@
     "@material-ui/pickers": "^3.3.10",
     "@mui/x-data-grid": "^4.0.0",
     "axios": "^0.21.3",
+    "d3": "^7.8.2",
     "dayjs": "^1.10.5",
     "file-saver": "^2.0.5",
     "i18next": "^20.3.1",
@@ -31,12 +32,11 @@
     "web-vitals": "^1.0.1"
   },
   "devDependencies": {
-    "@vitejs/plugin-react": "^2.2.0",
-    "@vitejs/plugin-react-refresh": "^1.3.6",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "12.1.2",
     "@testing-library/react-hooks": "^7.0.1",
     "@testing-library/user-event": "^12.1.10",
+    "@types/d3": "^7.4.0",
     "@types/file-saver": "^2.0.4",
     "@types/loadable__component": "^5.13.4",
     "@types/node": "^12.0.0",
@@ -47,6 +47,8 @@
     "@types/react-router-dom": "^5.3.3",
     "@types/react-syntax-highlighter": "^13.5.2",
     "@types/webpack-env": "^1.16.3",
+    "@vitejs/plugin-react": "^2.2.0",
+    "@vitejs/plugin-react-refresh": "^1.3.6",
     "@vitest/coverage-c8": "^0.25.0",
     "jsdom": "^20.0.2",
     "prettier": "2.3.2",

+ 4 - 0
client/public/env-config.js

@@ -2,4 +2,8 @@ window._env_ = {
   MILVUS_URL: '127.0.0.1:19530',
   HOST_URL: 'http://127.0.0.1:3000',
   IS_ELECTRON: '{{IS_ELECTRON}}',
+  WITH_PROMETHEUS: '',
+  PROMETHEUS_ADDRESS: '',
+  PROMETHEUS_INSTANCE_NAME: '',
+  PROMETHEUS_NAMESPACE: ''
 };

+ 10 - 7
client/src/App.tsx

@@ -5,18 +5,21 @@ import { RootProvider } from './context/Root';
 import { NavProvider } from './context/Navigation';
 import { AuthProvider } from './context/Auth';
 import { WebSocketProvider } from './context/WebSocket';
+import { PrometheusProvider } from './context/Prometheus';
 
 function App() {
   return (
     <AuthProvider>
       <RootProvider>
-        <WebSocketProvider>
-          <NavProvider>
-            <MuiPickersUtilsProvider utils={DayjsUtils}>
-              <Router></Router>
-            </MuiPickersUtilsProvider>
-          </NavProvider>
-        </WebSocketProvider>
+        <PrometheusProvider>
+          <WebSocketProvider>
+            <NavProvider>
+              <MuiPickersUtilsProvider utils={DayjsUtils}>
+                <Router></Router>
+              </MuiPickersUtilsProvider>
+            </NavProvider>
+          </WebSocketProvider>
+        </PrometheusProvider>
       </RootProvider>
     </AuthProvider>
   );

+ 9 - 2
client/src/components/customRadio/CustomRadio.tsx

@@ -3,9 +3,10 @@ import { FormGroup, FormControlLabel, Switch } from '@material-ui/core';
 
 export const CustomRadio = (props: {
   label: string;
+  defaultChecked?: boolean;
   handleChange: (checked: boolean) => void;
 }) => {
-  const { label, handleChange } = props;
+  const { label, defaultChecked = false, handleChange } = props;
   const onChange = (
     e: React.ChangeEvent<HTMLInputElement>,
     checked: boolean
@@ -15,7 +16,13 @@ export const CustomRadio = (props: {
   return (
     <FormGroup>
       <FormControlLabel
-        control={<Switch onChange={onChange} color="primary" />}
+        control={
+          <Switch
+            defaultChecked={defaultChecked}
+            onChange={onChange}
+            color="primary"
+          />
+        }
         label={label}
       />
     </FormGroup>

+ 9 - 0
client/src/consts/Localstorage.ts

@@ -2,3 +2,12 @@ export const SESSION = 'CLOUD_SESSION';
 export const MILVUS_ADDRESS = 'milvus-address';
 export const LAST_TIME_ADDRESS = 'last-time-address';
 
+export const LAST_TIME_WITH_PROMETHEUS = 'last-time-with-prometheus';
+export const LAST_TIME_PROMETHEUS_ADDRESS = 'last-time-prometheus-address';
+export const LAST_TIME_PROMETHEUS_INSTANCE = 'last-time-prometheus-instance';
+export const LAST_TIME_PROMETHEUS_NAMESPACE = 'last-time-prometheus-namespace';
+
+export const LAST_TIME_HEALTHY_THRESHOLD_CPU =
+  'last-time-healthy-threshold-cpu';
+export const LAST_TIME_HEALTHY_THRESHOLD_MEMORY =
+  'last-time-healthy-threshold-memory';

+ 1 - 0
client/src/consts/Milvus.tsx

@@ -217,6 +217,7 @@ export enum LOADING_STATE {
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;
 export const DEFAULT_MILVUS_PORT = 19530;
+export const DEFAULT_PROMETHEUS_PORT = 9090;
 
 export enum MILVUS_NODE_TYPE {
   ROOTCOORD = 'rootcoord',

+ 13 - 0
client/src/consts/Prometheus.tsx

@@ -0,0 +1,13 @@
+export const WITH_PROMETHEUS = (window as any)?._env_?.WITH_PROMETHEUS || '';
+
+export const PROMETHEUS_ADDRESS =
+  (window as any)?._env_?.PROMETHEUS_ADDRESS || '';
+
+export const PROMETHEUS_INSTANCE_NAME =
+  (window as any)?._env_?.PROMETHEUS_INSTANCE_NAME || '';
+
+export const PROMETHEUS_NAMESPACE =
+  (window as any)?._env_?.PROMETHEUS_NAMESPACE || '';
+
+export const DEFAULT_HEALTHY_THRESHOLD_CPU = 1;
+export const DEFAULT_HEALTHY_THRESHOLD_MEMORY = 8 * 1024 * 1024 * 1024;

+ 116 - 0
client/src/context/Prometheus.tsx

@@ -0,0 +1,116 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { PrometheusContextType } from './Types';
+import { authContext } from '../context/Auth';
+import {
+  LAST_TIME_WITH_PROMETHEUS,
+  LAST_TIME_PROMETHEUS_ADDRESS,
+  LAST_TIME_PROMETHEUS_INSTANCE,
+  LAST_TIME_PROMETHEUS_NAMESPACE,
+} from '../consts/Localstorage';
+import { formatPrometheusAddress } from '../utils/Format';
+import { PrometheusHttp } from '../http/Prometheus';
+import {
+  PROMETHEUS_ADDRESS,
+  PROMETHEUS_INSTANCE_NAME,
+  PROMETHEUS_NAMESPACE,
+  WITH_PROMETHEUS,
+} from '../consts/Prometheus';
+import { rootContext } from './Root';
+import { useTranslation } from 'react-i18next';
+
+export const prometheusContext = createContext<PrometheusContextType>({
+  withPrometheus: false,
+  setWithPrometheus: () => {},
+  isPrometheusReady: false,
+  prometheusAddress: '',
+  prometheusInstance: '',
+  prometheusNamespace: '',
+  setPrometheusAddress: () => {},
+  setPrometheusInstance: () => {},
+  setPrometheusNamespace: () => {},
+});
+
+const { Provider } = prometheusContext;
+export const PrometheusProvider = (props: { children: React.ReactNode }) => {
+  const { isAuth } = useContext(authContext);
+
+  const [withPrometheus, setWithPrometheus] = useState(
+    !!(
+      window.localStorage.getItem(LAST_TIME_WITH_PROMETHEUS) || WITH_PROMETHEUS
+    )
+  );
+  const [prometheusAddress, setPrometheusAddress] = useState(
+    window.localStorage.getItem(LAST_TIME_PROMETHEUS_ADDRESS) ||
+      PROMETHEUS_ADDRESS
+  );
+  const [prometheusInstance, setPrometheusInstance] = useState(
+    window.localStorage.getItem(LAST_TIME_PROMETHEUS_INSTANCE) ||
+      PROMETHEUS_INSTANCE_NAME
+  );
+  const [prometheusNamespace, setPrometheusNamespace] = useState(
+    window.localStorage.getItem(LAST_TIME_PROMETHEUS_NAMESPACE) ||
+      PROMETHEUS_NAMESPACE
+  );
+
+  const [isPrometheusReady, setIsPrometheusReady] = useState(false);
+
+  const { openSnackBar } = useContext(rootContext);
+  const { t: prometheusTrans } = useTranslation('prometheus');
+
+  useEffect(() => {
+    if (!isAuth) return;
+    if (withPrometheus) {
+      const prometheusAddressformat =
+        formatPrometheusAddress(prometheusAddress);
+      PrometheusHttp.setPrometheus({
+        prometheusAddress: prometheusAddressformat,
+        prometheusInstance,
+        prometheusNamespace,
+      })
+        .then(({ isReady }: { isReady: boolean }) => {
+          if (isReady) {
+            window.localStorage.setItem(LAST_TIME_WITH_PROMETHEUS, 'true');
+            window.localStorage.setItem(
+              LAST_TIME_PROMETHEUS_ADDRESS,
+              prometheusAddress
+            );
+            window.localStorage.setItem(
+              LAST_TIME_PROMETHEUS_INSTANCE,
+              prometheusInstance
+            );
+            window.localStorage.setItem(
+              LAST_TIME_PROMETHEUS_NAMESPACE,
+              prometheusNamespace
+            );
+          } else {
+            openSnackBar(prometheusTrans('invalid'), 'error');
+          }
+          setIsPrometheusReady(isReady);
+        })
+        .catch(err => {
+          openSnackBar(prometheusTrans('invalid'), 'error');
+          setIsPrometheusReady(false);
+        });
+    } else {
+      setIsPrometheusReady(false);
+    }
+  }, [isAuth, setIsPrometheusReady]);
+
+  return (
+    <Provider
+      value={{
+        withPrometheus,
+        setWithPrometheus,
+        isPrometheusReady,
+        prometheusAddress,
+        prometheusInstance,
+        prometheusNamespace,
+        setPrometheusAddress,
+        setPrometheusInstance,
+        setPrometheusNamespace,
+      }}
+    >
+      {props.children}
+    </Provider>
+  );
+};

+ 12 - 0
client/src/context/Types.ts

@@ -61,6 +61,18 @@ export type AuthContextType = {
   setIsAuth: Dispatch<SetStateAction<boolean>>;
 };
 
+export type PrometheusContextType = {
+  withPrometheus: boolean;
+  setWithPrometheus: Dispatch<SetStateAction<boolean>>;
+  isPrometheusReady: boolean;
+  prometheusAddress: string;
+  prometheusInstance: string;
+  prometheusNamespace: string;
+  setPrometheusAddress: Dispatch<SetStateAction<string>>;
+  setPrometheusInstance: Dispatch<SetStateAction<string>>;
+  setPrometheusNamespace: Dispatch<SetStateAction<string>>;
+};
+
 export type NavContextType = {
   navInfo: NavInfo;
   setNavInfo: (param: NavInfo) => void;

+ 6 - 3
client/src/http/BaseModel.ts

@@ -5,6 +5,7 @@ type findParamsType = {
   method?: Method;
   path: string;
   params: { [x: string]: any };
+  timeout?: number;
 };
 
 type updateParamsType = {
@@ -41,12 +42,14 @@ export default class BaseModel {
   }
 
   static async search(data: findParamsType) {
-    const { method = 'get', params = {}, path = '' } = data;
-    const res = await http({
+    const { method = 'get', params = {}, path = '', timeout } = data;
+    const httpConfig = {
       method,
       url: path,
       params,
-    });
+    } as any;
+    if (timeout) httpConfig.timeout = timeout;
+    const res = await http(httpConfig);
     return res.data.data;
   }
 

+ 42 - 0
client/src/http/Prometheus.ts

@@ -0,0 +1,42 @@
+import BaseModel from './BaseModel';
+
+export class PrometheusHttp extends BaseModel {
+  static SET_PROMETHEUS_URL = '/prometheus/setPrometheus';
+  static GET_MILVUS_HEALTHY_DATA_URL = '/prometheus/getMilvusHealthyData';
+
+  constructor(props: {}) {
+    super(props);
+    Object.assign(this, props);
+  }
+
+  static setPrometheus({
+    prometheusAddress,
+    prometheusInstance,
+    prometheusNamespace,
+  }: {
+    prometheusAddress: string;
+    prometheusInstance: string;
+    prometheusNamespace: string;
+  }) {
+    return super.search({
+      path: PrometheusHttp.SET_PROMETHEUS_URL,
+      params: { prometheusAddress, prometheusInstance, prometheusNamespace },
+      timeout: 1000,
+    });
+  }
+
+  static getHealthyData({
+    start,
+    end,
+    step,
+  }: {
+    start: number;
+    end: number;
+    step: number;
+  }) {
+    return super.search({
+      path: PrometheusHttp.GET_MILVUS_HEALTHY_DATA_URL,
+      params: { start, end, step },
+    });
+  }
+}

+ 4 - 0
client/src/i18n/cn/common.ts

@@ -6,6 +6,10 @@ const commonTrans = {
     username: 'Username',
     password: 'Password',
     optional: '(optional)',
+    prometheus: 'Prometheus',
+    prometheusAddress: 'Prometheus Address',
+    prometheusInstance: 'Prometheus Instance',
+    prometheusNamespace: 'Prometheus Namespace',
     ssl: 'SSL',
   },
   status: {

+ 10 - 0
client/src/i18n/cn/prometheus.ts

@@ -0,0 +1,10 @@
+const prometheusTrans = {
+  ready: 'Prometheus is ready.',
+  invalid: 'Prometheus configuration is invalid.',
+  
+  totalCount: 'Total Count',
+  searchCount: 'Search Count',
+  searchLatency: 'Search Latency',
+};
+
+export default prometheusTrans;

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

@@ -6,6 +6,10 @@ const commonTrans = {
     username: 'Username',
     password: 'Password',
     optional: '(optional)',
+    prometheus: 'Prometheus',
+    prometheusAddress: 'Prometheus Address',
+    prometheusInstance: 'Prometheus Instance',
+    prometheusNamespace: 'Prometheus Namespace',
     ssl: 'SSL',
   },
   status: {

+ 10 - 0
client/src/i18n/en/prometheus.ts

@@ -0,0 +1,10 @@
+const prometheusTrans = {
+  ready: 'Prometheus is ready.',
+  invalid: 'Prometheus configuration is invalid.',
+
+  totalCount: 'Total Count',
+  searchCount: 'Search Count',
+  searchLatency: 'Search Latency',
+};
+
+export default prometheusTrans;

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

@@ -29,6 +29,8 @@ import systemViewTransEn from './en/systemView';
 import systemViewTransCn from './cn/systemView';
 import userTransEn from './en/user';
 import userTransCn from './cn/user';
+import prometheusTransEn from './en/prometheus';
+import prometheusTransCn from './cn/prometheus';
 
 export const resources = {
   cn: {
@@ -46,6 +48,7 @@ export const resources = {
     search: searchCn,
     systemView: systemViewTransCn,
     user: userTransCn,
+    prometheus: prometheusTransCn,
   },
   en: {
     translation: commonEn,
@@ -62,6 +65,7 @@ export const resources = {
     search: searchEn,
     systemView: systemViewTransEn,
     user: userTransEn,
+    prometheus: prometheusTransEn,
   },
 };
 

+ 69 - 1
client/src/pages/connect/AuthForm.tsx

@@ -17,6 +17,7 @@ import { MILVUS_ADDRESS, LAST_TIME_ADDRESS } from '../../consts/Localstorage';
 import { CODE_STATUS } from '../../consts/Http';
 import { MILVUS_URL } from '../../consts/Milvus';
 import { CustomRadio } from '../../components/customRadio/CustomRadio';
+import { prometheusContext } from '../../context/Prometheus';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -45,7 +46,7 @@ const useStyles = makeStyles((theme: Theme) => ({
     display: 'block',
   },
   input: {
-    margin: theme.spacing(3, 0, 0.5),
+    margin: theme.spacing(0.5, 0, 0),
   },
   sslWrapper: {
     display: 'flex',
@@ -133,6 +134,56 @@ export const AuthForm = (props: any) => {
     ];
   }, [form, attuTrans, warningTrans, classes.input]);
 
+  const {
+    withPrometheus,
+    setWithPrometheus,
+    prometheusAddress,
+    prometheusInstance,
+    prometheusNamespace,
+    setPrometheusAddress,
+    setPrometheusInstance,
+    setPrometheusNamespace,
+  } = useContext(prometheusContext);
+
+  const prometheusConfigs: ITextfieldConfig[] = useMemo(
+    () => [
+      {
+        label: `${attuTrans.prometheusAddress}`,
+        key: 'prometheus_address',
+        onChange: setPrometheusAddress,
+        variant: 'filled',
+        className: classes.input,
+        placeholder: attuTrans.prometheusAddress,
+        fullWidth: true,
+
+        defaultValue: prometheusAddress,
+      },
+      {
+        label: `${attuTrans.prometheusNamespace}`,
+        key: 'prometheus_namespace',
+        onChange: setPrometheusNamespace,
+        variant: 'filled',
+        className: classes.input,
+        placeholder: attuTrans.prometheusNamespace,
+        fullWidth: true,
+
+        defaultValue: prometheusNamespace,
+      },
+      {
+        label: `${attuTrans.prometheusInstance}`,
+        key: 'prometheus_instance',
+        onChange: setPrometheusInstance,
+        variant: 'filled',
+        className: classes.input,
+        placeholder: attuTrans.prometheusInstance,
+        fullWidth: true,
+
+        defaultValue: prometheusInstance,
+      },
+    ],
+    []
+  );
+
   const handleConnect = async () => {
     const address = formatAddress(form.address);
     try {
@@ -184,6 +235,23 @@ export const AuthForm = (props: any) => {
             handleChange={(val: boolean) => handleInputChange('ssl', val)}
           />
         </div>
+        <div className={classes.sslWrapper}>
+          <CustomRadio
+            defaultChecked={withPrometheus}
+            label={attuTrans.prometheus}
+            handleChange={setWithPrometheus}
+          />
+        </div>
+        {withPrometheus &&
+          prometheusConfigs.map(v => (
+            <CustomInput
+              type="text"
+              textConfig={v}
+              checkValid={checkIsValid}
+              validInfo={validation}
+              key={v.label}
+            />
+          ))}
         <CustomButton type="submit" variant="contained" disabled={btnDisabled}>
           {btnTrans('connect')}
         </CustomButton>

+ 6 - 1
client/src/pages/index.tsx

@@ -10,6 +10,7 @@ import icons from '../components/icons/Icons';
 import { authContext } from '../context/Auth';
 import { rootContext } from '../context/Root';
 import Overview from '../pages/overview/Overview';
+import { prometheusContext } from '../context/Prometheus';
 
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
@@ -45,6 +46,7 @@ const useStyles = makeStyles((theme: Theme) =>
 function Index() {
   const navigate = useNavigate();
   const { isAuth } = useContext(authContext);
+  const { isPrometheusReady } = useContext(prometheusContext);
   const { versionInfo } = useContext(rootContext);
   const { t: navTrans } = useTranslation('nav');
   const classes = useStyles();
@@ -91,7 +93,10 @@ function Index() {
     {
       icon: icons.navSystem,
       label: navTrans('system'),
-      onClick: () => navigate('/system'),
+      onClick: () =>
+        isPrometheusReady
+          ? navigate('/system_healthy')
+          : navigate('/system'),
       iconActiveClass: 'normal',
       iconNormalClass: 'active',
     },

+ 203 - 0
client/src/pages/systemHealthy/HealthyIndexDetailView.tsx

@@ -0,0 +1,203 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { CHART_WIDTH, LINE_CHART_SMALL_HEIGHT } from './consts';
+import HealthyIndexRow from './HealthyIndexRow';
+import LineChartSmall from './LineChartSmall';
+import { ENodeService, INodeTreeStructure, IThreshold } from './Types';
+import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
+import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
+import { Dispatch, SetStateAction, useState } from 'react';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  mainView: {
+    width: '100%',
+    marginTop: '16px',
+  },
+  healthyIndexItem: {
+    display: 'flex',
+    marginTop: '8px',
+    justifyContent: 'space-between',
+  },
+  healthyIndexLabel: {
+    fontWeight: 500,
+    fontSize: '12px',
+    color: '#444',
+    display: 'flex',
+    alignItems: 'center',
+    cursor: 'pointer',
+  },
+  healthyIndexLabelText: {},
+  healthyIndexRow: {
+    width: `${CHART_WIDTH}px`,
+  },
+  chartItem: {
+    margin: '8px 0',
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  chartLabel: {
+    width: `50px`,
+    paddingLeft: '20px',
+    fontSize: '12px',
+    fontWeight: 500,
+    color: '#444',
+  },
+  chart: {
+    height: `${LINE_CHART_SMALL_HEIGHT}px`,
+    width: `${CHART_WIDTH}px`,
+  },
+}));
+
+const HealthyIndexTreeItem = ({
+  node,
+  threshold,
+}: {
+  node: INodeTreeStructure;
+  threshold: IThreshold;
+}) => {
+  const classes = getStyles();
+  const [open, setOpen] = useState(false);
+  return (
+    <>
+      <div
+        key={`${node.service}-${node.type}-${node.label}`}
+        className={classes.healthyIndexItem}
+      >
+        <div
+          className={classes.healthyIndexLabel}
+          onClick={() => setOpen(!open)}
+        >
+          {open ? (
+            <KeyboardArrowDownIcon fontSize="small" />
+          ) : (
+            <KeyboardArrowRightIcon fontSize="small" />
+          )}
+          <div className={classes.healthyIndexLabelText}>{`${
+            node.type
+          }-${node.label.slice(-5)}`}</div>
+        </div>
+        <div className={classes.healthyIndexRow}>
+          <HealthyIndexRow statusList={node.healthyStatus} />
+        </div>
+      </div>
+      {open && (
+        <>
+          <div className={classes.chartItem}>
+            <div className={classes.chartLabel}>cpu</div>
+            <div className={classes.chart}>
+              <LineChartSmall
+                data={node.cpu || []}
+                format={(v: number) => v.toFixed(3)}
+                unit={'Core'}
+                threshold={threshold.cpu}
+              />
+            </div>
+          </div>
+          <div className={classes.chartItem}>
+            <div className={classes.chartLabel}>memory</div>
+            <div className={classes.chart}>
+              <LineChartSmall
+                data={node.memory || []}
+                format={(v: number) => (v / 1024 / 1024 / 1024).toFixed(1)}
+                unit={'GB'}
+                threshold={threshold.memory}
+              />
+            </div>
+          </div>
+        </>
+      )}
+    </>
+  );
+};
+
+const HealthyIndexWithTree = ({
+  nodeTree,
+  setSelectedService,
+  threshold,
+}: {
+  nodeTree: INodeTreeStructure;
+  setSelectedService: Dispatch<SetStateAction<ENodeService>>;
+  threshold: IThreshold;
+}) => {
+  const classes = getStyles();
+  return (
+    <div className={classes.mainView}>
+      {!!nodeTree && (
+        <div className={classes.healthyIndexItem}>
+          <div
+            className={classes.healthyIndexLabel}
+            onClick={() => setSelectedService(ENodeService.root)}
+          >
+            {nodeTree.label}
+          </div>
+          <div className={classes.healthyIndexRow}>
+            <HealthyIndexRow statusList={nodeTree?.healthyStatus || []} />
+          </div>
+        </div>
+      )}
+      {!!nodeTree &&
+        nodeTree.children.map(node => (
+          <HealthyIndexTreeItem
+            key={node.label}
+            node={node}
+            threshold={threshold}
+          />
+        ))}
+    </div>
+  );
+};
+
+const HealthyIndexWithoutTree = ({
+  nodeTree,
+  setSelectedService,
+}: {
+  nodeTree: INodeTreeStructure;
+  setSelectedService: Dispatch<SetStateAction<ENodeService>>;
+}) => {
+  const classes = getStyles();
+  return (
+    <div className={classes.mainView}>
+      {nodeTree.children.map(node => (
+        <div
+          key={`${node.service}-${node.type}`}
+          className={classes.healthyIndexItem}
+        >
+          <div
+            className={classes.healthyIndexLabel}
+            onClick={() => setSelectedService(node.service)}
+          >
+            {node.label}
+          </div>
+          <div className={classes.healthyIndexRow}>
+            <HealthyIndexRow statusList={node.healthyStatus} />
+          </div>
+        </div>
+      ))}
+    </div>
+  );
+};
+
+const HealthyIndexDetailView = ({
+  nodeTree,
+  setSelectedService,
+  threshold,
+}: {
+  nodeTree: INodeTreeStructure;
+  setSelectedService: Dispatch<SetStateAction<ENodeService>>;
+  threshold: IThreshold;
+}) => {
+  return nodeTree.service === ENodeService.milvus ? (
+    <HealthyIndexWithoutTree
+      nodeTree={nodeTree}
+      setSelectedService={setSelectedService}
+    />
+  ) : (
+    <HealthyIndexWithTree
+      nodeTree={nodeTree}
+      setSelectedService={setSelectedService}
+      threshold={threshold}
+    />
+  );
+};
+
+export default HealthyIndexDetailView;

+ 58 - 0
client/src/pages/systemHealthy/HealthyIndexLegend.tsx

@@ -0,0 +1,58 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { HEALTHY_INDEX_ROW_HEIGHT, HEALTHY_STATUS_COLORS } from './consts';
+import { EHealthyStatus } from './Types';
+
+const legendData = [
+  {
+    label: 'NoData',
+    value: EHealthyStatus.noData,
+  },
+  {
+    label: 'Healthy',
+    value: EHealthyStatus.healthy,
+  },
+  {
+    label: 'Warning',
+    value: EHealthyStatus.warning,
+  },
+  {
+    label: 'Failed',
+    value: EHealthyStatus.failed,
+  },
+];
+
+const getStyles = makeStyles((theme: Theme) => ({
+  legendItem: {
+    display: 'flex',
+    marginLeft: '12px',
+    fontSize: '10px',
+    alignItems: 'flex-end',
+  },
+  legendIcon: {
+    width: '16px',
+    borderRadius: '1px',
+  },
+  legendText: { marginLeft: '8px', fontWeight: 500, color: '#666' },
+}));
+
+const HealthyIndexLegend = () => {
+  const classes = getStyles();
+  return (
+    <>
+      {legendData.map(legend => (
+        <div key={legend.label} className={classes.legendItem}>
+          <div
+            className={classes.legendIcon}
+            style={{
+              background: HEALTHY_STATUS_COLORS[legend.value],
+              height: `${HEALTHY_INDEX_ROW_HEIGHT * 0.8}px`,
+            }}
+          ></div>
+          <div className={classes.legendText}>{legend.label}</div>
+        </div>
+      ))}
+    </>
+  );
+};
+
+export default HealthyIndexLegend;

+ 141 - 0
client/src/pages/systemHealthy/HealthyIndexOverview.tsx

@@ -0,0 +1,141 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { Dispatch, SetStateAction } from 'react';
+import {
+  CHART_WIDTH,
+  LINE_CHART_LARGE_HEIGHT,
+  MAIN_VIEW_WIDTH,
+} from './consts';
+import HealthyIndexDetailView from './HealthyIndexDetailView';
+import HealthyIndexLegend from './HealthyIndexLegend';
+import LineChartLarge from './LineChartLarge';
+import ThresholdSetting from './ThresholdSetting';
+import TimeRangeTabs from './TimeRangeTabs';
+import {
+  ENodeService,
+  ILineChartData,
+  INodeTreeStructure,
+  IThreshold,
+  ITimeRangeOption,
+} from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    width: `${MAIN_VIEW_WIDTH}px`,
+    overflow: 'auto',
+    padding: '8px 56px 60px 24px',
+    fontSize: '14px',
+  },
+  headerContent: {
+    display: 'grid',
+    gridTemplateColumns: '1fr auto',
+  },
+  titleContainer: {},
+  title: {
+    display: 'flex',
+    alignItems: 'flex-end',
+  },
+  titleMain: { fontSize: '18px', fontWeight: 500, cursor: 'pointer' },
+  titleExt: { fontSize: '18px', fontWeight: 500, marginLeft: '8px' },
+  timeRangeTabs: {
+    fontSize: '12px',
+  },
+  legendContainer: {
+    display: 'flex',
+    alignItems: 'flex-end',
+  },
+  settingIcon: { marginLeft: '12px', display: 'flex', alignItems: 'flex-end' },
+
+  chartView: { width: '100%', marginTop: '24px' },
+  chartItem: {
+    margin: '16px 0',
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'flex-end',
+  },
+  chartLabel: {
+    width: `50px`,
+    fontWeight: 500,
+    color: '#444',
+  },
+  chart: {
+    height: `${LINE_CHART_LARGE_HEIGHT}px`,
+    width: `${CHART_WIDTH}px`,
+  },
+}));
+
+const HealthyIndexOverview = ({
+  selectedNode,
+  lineChartsData,
+  threshold,
+  setThreshold,
+  timeRange,
+  setTimeRange,
+  setSelectedService,
+}: {
+  selectedNode: INodeTreeStructure;
+  lineChartsData: ILineChartData[];
+  threshold: IThreshold;
+  setThreshold: (threshold: IThreshold) => void;
+  timeRange: ITimeRangeOption;
+  setTimeRange: Dispatch<SetStateAction<ITimeRangeOption>>;
+  setSelectedService: Dispatch<SetStateAction<ENodeService>>;
+}) => {
+  const classes = getStyles();
+  return (
+    <div className={classes.root}>
+      <div className={classes.headerContent}>
+        <div className={classes.titleContainer}>
+          <div className={classes.title}>
+            <div
+              className={classes.titleMain}
+              onClick={() => setSelectedService(ENodeService.root)}
+            >
+              Healthy Status
+            </div>
+            {selectedNode.service !== ENodeService.milvus && (
+              <div className={classes.titleExt}>
+                {`> ${selectedNode.service}`}
+              </div>
+            )}
+          </div>
+          <div className={classes.timeRangeTabs}>
+            <TimeRangeTabs timeRange={timeRange} setTimeRange={setTimeRange} />
+          </div>
+        </div>
+        <div className={classes.legendContainer}>
+          <HealthyIndexLegend />
+          <div className={classes.settingIcon}>
+            <ThresholdSetting
+              threshold={threshold}
+              setThreshold={setThreshold}
+            />
+          </div>
+        </div>
+      </div>
+      <HealthyIndexDetailView
+        nodeTree={selectedNode}
+        setSelectedService={setSelectedService}
+        threshold={threshold}
+      />
+      {selectedNode.service === ENodeService.milvus && (
+        <div className={classes.chartView}>
+          <div className={classes.titleMain}>Search Query History</div>
+          {lineChartsData.map(chartData => (
+            <div key={chartData.label} className={classes.chartItem}>
+              <div className={classes.chartLabel}>{chartData.label}</div>
+              <div className={classes.chart}>
+                <LineChartLarge
+                  data={chartData.data}
+                  format={chartData.format}
+                  unit={chartData.unit}
+                />
+              </div>
+            </div>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default HealthyIndexOverview;

+ 32 - 0
client/src/pages/systemHealthy/HealthyIndexRow.tsx

@@ -0,0 +1,32 @@
+import {
+  CHART_WIDTH,
+  HEALTHY_INDEX_ROW_GAP_RATIO,
+  HEALTHY_INDEX_ROW_HEIGHT,
+  HEALTHY_STATUS_COLORS,
+} from './consts';
+import { EHealthyStatus } from './Types';
+
+const HealthyIndexRow = ({ statusList }: { statusList: EHealthyStatus[] }) => {
+  const length = statusList.length;
+  const stautsItemWidth = length === 0 ? 0 : CHART_WIDTH / length;
+  const statusBlockGap = stautsItemWidth * HEALTHY_INDEX_ROW_GAP_RATIO;
+  const statusBlockWidth = stautsItemWidth * (1 - HEALTHY_INDEX_ROW_GAP_RATIO);
+  return (
+    <svg width={CHART_WIDTH} height={HEALTHY_INDEX_ROW_HEIGHT}>
+      {statusList.map((status, i) => (
+        <rect
+          key={`status-${i}`}
+          x={i * stautsItemWidth + statusBlockGap / 2}
+          y={0}
+          rx={1}
+          ry={1}
+          width={statusBlockWidth}
+          height={HEALTHY_INDEX_ROW_HEIGHT}
+          fill={HEALTHY_STATUS_COLORS[status]}
+        />
+      ))}
+    </svg>
+  );
+};
+
+export default HealthyIndexRow;

+ 95 - 0
client/src/pages/systemHealthy/LineChartLarge.tsx

@@ -0,0 +1,95 @@
+import * as d3 from 'd3';
+import {
+  CHART_WIDTH,
+  LINE_CHART_LARGE_HEIGHT,
+  LINE_COLOR,
+  LINE_LABEL_FONT_SIZE,
+  LINE_LABEL_Y_PADDING,
+  LINE_WIDTH,
+} from './consts';
+
+const LineChartLarge = ({
+  data,
+  format = d => d,
+  unit = '',
+}: {
+  data: number[];
+  format?: (d: any) => string;
+  unit?: string;
+}) => {
+  const length = data.length;
+  const width = CHART_WIDTH;
+  const height = LINE_CHART_LARGE_HEIGHT - 3;
+  const fontSize = LINE_LABEL_FONT_SIZE;
+
+  const xDomain = [0, length];
+  const xRange = [0, CHART_WIDTH];
+  let maxData = d3.max(data, d => d) as number;
+  maxData = maxData === 0 ? 1 : maxData;
+
+  const yDomain = [0, maxData * 1.1];
+  const yRange = [height, 0];
+
+  const xScale = d3.scaleLinear(xDomain, xRange);
+  const yScale = d3.scaleLinear(yDomain, yRange);
+
+  const nodes = data
+    .map((d, i) => (d >= 0 ? [xScale(i + 0.5), yScale(d)] : undefined))
+    .filter(a => a) as [number, number][];
+
+  const line = d3
+    .line()
+    .curve(d3.curveBumpX)
+    .x(d => d[0])
+    .y(d => d[1]);
+
+  return (
+    <svg
+      width={width}
+      height={height}
+      style={{ overflow: 'visible' }}
+      fontSize={fontSize}
+      fontWeight={500}
+    >
+      <g className="x-axis">
+        <line x1={0} y1={height} x2={width} y2={height} stroke="#666" />
+      </g>
+      <g className="y-axis">
+        <line x1={0} y1={0} x2={0} y2={height} stroke="#666" />
+        <text x={-LINE_LABEL_Y_PADDING} y={height} textAnchor="end" fill="#555">
+          {0}
+        </text>
+        <text
+          x={-LINE_LABEL_Y_PADDING}
+          y={fontSize}
+          textAnchor="end"
+          fill="#555"
+        >
+          {format(maxData)}
+        </text>
+        {unit && (
+          <text
+            x={-LINE_LABEL_Y_PADDING}
+            y={fontSize * 2}
+            textAnchor="end"
+            fill={'#666'}
+            fontSize={fontSize - 2}
+          >
+            ({unit})
+          </text>
+        )}
+      </g>
+      <g className="line">
+        <path
+          d={line(nodes) as any}
+          fill="none"
+          stroke={`${LINE_COLOR}`}
+          strokeWidth={LINE_WIDTH}
+          opacity={0.8}
+          strokeLinecap="round"
+        />
+      </g>
+    </svg>
+  );
+};
+export default LineChartLarge;

+ 125 - 0
client/src/pages/systemHealthy/LineChartSmall.tsx

@@ -0,0 +1,125 @@
+import * as d3 from 'd3';
+import {
+  CHART_WIDTH,
+  HEALTHY_STATUS_COLORS,
+  LINE_CHART_LARGE_HEIGHT,
+  LINE_CHART_SMALL_HEIGHT,
+  LINE_COLOR,
+  LINE_LABEL_FONT_SIZE,
+  LINE_LABEL_Y_PADDING,
+  LINE_SMALL_LABEL_FONT_SIZE,
+  LINE_WIDTH,
+} from './consts';
+import { EHealthyStatus } from './Types';
+
+const LineChartSmall = ({
+  data,
+  format = d => d,
+  unit = '',
+  threshold,
+}: {
+  data: number[];
+  format?: (d: any) => string;
+  unit?: string;
+  threshold: number;
+}) => {
+  const length = data.length;
+  const width = CHART_WIDTH;
+  const height = LINE_CHART_SMALL_HEIGHT - 3;
+  const fontSize = LINE_SMALL_LABEL_FONT_SIZE;
+
+  const xDomain = [0, length];
+  const xRange = [0, CHART_WIDTH];
+  let maxData = d3.max(data, d => d) as number;
+  maxData = maxData === 0 ? 1 : maxData;
+
+  const yDomain = [0, maxData * 1.1];
+  const yRange = [height, 0];
+
+  const xScale = d3.scaleLinear(xDomain, xRange);
+  const yScale = d3.scaleLinear(yDomain, yRange);
+
+  const nodes = data
+    .map((d, i) => (d >= 0 ? [xScale(i + 0.5), yScale(d)] : undefined))
+    .filter(a => a) as [number, number][];
+
+  const line = d3
+    .line()
+    .curve(d3.curveBumpX)
+    .x(d => d[0])
+    .y(d => d[1]);
+
+  return (
+    <svg
+      width={width}
+      height={height}
+      style={{ overflow: 'visible' }}
+      fontSize={fontSize}
+      fontWeight={500}
+    >
+      <g className="x-axis">
+        <line x1={0} y1={height} x2={width} y2={height} stroke="#666" />
+        <line
+          x1={width - LINE_LABEL_Y_PADDING}
+          y1={yScale(maxData)}
+          x2={width}
+          y2={yScale(maxData)}
+          stroke="#666"
+          strokeWidth="2"
+        />
+      </g>
+      <g className="y-axis">
+        <text
+          x={width + LINE_LABEL_Y_PADDING}
+          y={height}
+          textAnchor="start"
+          fill="#555"
+        >
+          {0}
+        </text>
+        <text
+          x={width + LINE_LABEL_Y_PADDING}
+          y={yScale(maxData) + 3}
+          textAnchor="start"
+          fill="#555"
+        >
+          {format(maxData)}
+        </text>
+        {unit && (
+          <text
+            x={width + LINE_LABEL_Y_PADDING}
+            y={yScale(maxData) + 3 + fontSize}
+            textAnchor="start"
+            fontSize={fontSize - 2}
+            fill="#555"
+          >
+            ({unit})
+          </text>
+        )}
+      </g>
+      <g className="line">
+        {maxData >= threshold && (
+          <line
+            x1={xScale(0.5)}
+            y1={yScale(threshold)}
+            x2={xScale(data.length - 0.5)}
+            y2={yScale(threshold)}
+            stroke={HEALTHY_STATUS_COLORS[EHealthyStatus.warning]}
+            strokeWidth={LINE_WIDTH + 1}
+            strokeLinecap="round"
+            strokeDasharray={"6 8"}
+          />
+        )}
+        <path
+          d={line(nodes) as any}
+          fill="none"
+          stroke={LINE_COLOR}
+          strokeWidth={LINE_WIDTH}
+          opacity={0.8}
+          strokeLinecap="round"
+        />
+      </g>
+    </svg>
+  );
+};
+export default LineChartSmall;

+ 174 - 0
client/src/pages/systemHealthy/SystemHealthyView.tsx

@@ -0,0 +1,174 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { useEffect, useMemo, useState } from 'react';
+import { useNavigationHook } from '../../hooks/Navigation';
+import { useInterval } from '../../hooks/SystemView';
+import { PrometheusHttp } from '../../http/Prometheus';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+import {
+  ENodeService,
+  ILineChartData,
+  INodeTreeStructure,
+  IPrometheusAllData,
+  IThreshold,
+  ITimeRangeOption,
+} from './Types';
+import Topology from './Topology';
+import { reconNodeTree } from './dataHandler';
+import HealthyIndexOverview from './HealthyIndexOverview';
+import { timeRangeOptions } from './consts';
+import {
+  LAST_TIME_HEALTHY_THRESHOLD_CPU,
+  LAST_TIME_HEALTHY_THRESHOLD_MEMORY,
+} from '../../consts/Localstorage';
+import {
+  DEFAULT_HEALTHY_THRESHOLD_CPU,
+  DEFAULT_HEALTHY_THRESHOLD_MEMORY,
+} from '../../consts/Prometheus';
+import { useTranslation } from 'react-i18next';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    fontFamily: 'Roboto',
+    margin: '16px 40px',
+    position: 'relative',
+    height: '88%',
+    display: 'flex',
+    flexDirection: 'column',
+  },
+  mainView: {
+    borderRadius: '8px',
+    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+    display: 'grid',
+    gridTemplateColumns: '1fr auto',
+    height: '100%',
+  },
+}));
+
+const SystemHealthyView = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
+
+  const classes = getStyles();
+
+  const INTERVAL = 60000;
+  const [timeRange, setTimeRange] = useState<ITimeRangeOption>(
+    timeRangeOptions[2]
+  );
+
+  const [threshold, setThreshold] = useState<IThreshold>({
+    cpu: +(
+      window.localStorage.getItem(LAST_TIME_HEALTHY_THRESHOLD_CPU) ||
+      DEFAULT_HEALTHY_THRESHOLD_CPU
+    ),
+    memory: +(
+      window.localStorage.getItem(LAST_TIME_HEALTHY_THRESHOLD_MEMORY) ||
+      DEFAULT_HEALTHY_THRESHOLD_MEMORY
+    ),
+  });
+  const changeThreshold = (threshold: IThreshold) => {
+    window.localStorage.setItem(
+      LAST_TIME_HEALTHY_THRESHOLD_CPU,
+      `${threshold.cpu}`
+    );
+    window.localStorage.setItem(
+      LAST_TIME_HEALTHY_THRESHOLD_MEMORY,
+      `${threshold.memory}`
+    );
+    setThreshold(threshold);
+  };
+  const [prometheusData, setPrometheusData] = useState<IPrometheusAllData>();
+  const nodeTree = useMemo<INodeTreeStructure>(
+    () =>
+      prometheusData
+        ? reconNodeTree(prometheusData, threshold)
+        : ({
+            children: [] as INodeTreeStructure[],
+          } as INodeTreeStructure),
+    [prometheusData, threshold]
+  );
+
+  const { t: prometheusTrans } = useTranslation('prometheus');
+  const lineChartsData = useMemo<ILineChartData[]>(
+    () =>
+      prometheusData
+        ? [
+            {
+              label: prometheusTrans('totalCount'),
+              data: prometheusData.totalVectorsCount,
+            },
+            {
+              label: prometheusTrans('searchCount'),
+              data: prometheusData.searchVectorsCount,
+            },
+            {
+              label: prometheusTrans('searchLatency'),
+              data: prometheusData.sqLatency,
+              format: d => d.toFixed(0),
+              unit: 'ms',
+            },
+          ]
+        : [],
+    [prometheusData]
+  );
+
+  const updateData = async () => {
+    const curT = new Date().getTime();
+    const result = (await PrometheusHttp.getHealthyData({
+      start: curT - timeRange.value,
+      end: curT,
+      step: timeRange.step,
+    })) as IPrometheusAllData;
+    setPrometheusData(result);
+  };
+
+  useEffect(() => {
+    updateData();
+  }, [timeRange]);
+
+  useInterval(() => {
+    updateData();
+  }, INTERVAL);
+
+  const hasDetailServices = [
+    ENodeService.index,
+    ENodeService.query,
+    ENodeService.data,
+  ];
+
+  const [selectedService, setSelectedService] = useState<ENodeService>(
+    ENodeService.root
+  );
+
+  const selectedNode = useMemo<INodeTreeStructure>(() => {
+    if (hasDetailServices.indexOf(selectedService) >= 0) {
+      return nodeTree.children.find(
+        node => node.service === selectedService
+      ) as INodeTreeStructure;
+    } else return nodeTree;
+  }, [selectedService, nodeTree]);
+
+  return (
+    <div className={classes.root}>
+      {!!prometheusData && (
+        <div className={classes.mainView}>
+          <Topology
+            nodeTree={nodeTree}
+            selectedService={selectedService}
+            onClick={setSelectedService}
+          />
+
+          <HealthyIndexOverview
+            selectedNode={selectedNode}
+            lineChartsData={lineChartsData}
+            threshold={threshold}
+            setThreshold={changeThreshold}
+            timeRange={timeRange}
+            setTimeRange={setTimeRange}
+            setSelectedService={setSelectedService}
+          />
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default SystemHealthyView;

+ 177 - 0
client/src/pages/systemHealthy/ThresholdSetting.tsx

@@ -0,0 +1,177 @@
+import {
+  Button,
+  Dialog,
+  DialogTitle,
+  Input,
+  List,
+  ListItem,
+  makeStyles,
+  Theme,
+  Typography,
+} from '@material-ui/core';
+import { Dispatch, SetStateAction, useMemo, useState } from 'react';
+import CustomInput from '../../components/customInput/CustomInput';
+import { ITextfieldConfig } from '../../components/customInput/Types';
+import { useFormValidation } from '../../hooks/Form';
+import { formatForm } from '../../utils/Form';
+import { HEALTHY_STATUS_COLORS } from './consts';
+import { EHealthyStatus, IThreshold } from './Types';
+import SettingsOutlinedIcon from '@material-ui/icons/SettingsOutlined';
+import CustomButton from '../../components/customButton/CustomButton';
+export interface SimpleDialogProps {
+  open: boolean;
+  selectedValue: string;
+  onClose: (value: string) => void;
+}
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    padding: '24px 32px',
+    width: '360px',
+    display: 'flex',
+    flexDirection: 'column',
+  },
+  note: {
+    fontWeight: 500,
+    color: '#444',
+    fontSize: '14px',
+    margin: theme.spacing(1, 0, 2),
+  },
+  input: {
+    margin: theme.spacing(0.5, 0, 0),
+  },
+  button: {
+    alignSelf: 'flex-end',
+  },
+}));
+
+function ThresholdSettingDialog({
+  onClose,
+  open,
+  threshold,
+  setThreshold,
+}: {
+  open: boolean;
+  onClose: () => void;
+  threshold: IThreshold;
+  setThreshold: (threshold: IThreshold) => void;
+}) {
+  const classes = getStyles();
+  const handleClose = () => {
+    setThreshold({ ...form, memory: form.memory * 1024 * 1024 * 1024 });
+    onClose();
+  };
+
+  const [form, setForm] = useState<IThreshold>({
+    cpu: threshold.cpu,
+    memory: threshold.memory / 1024 / 1024 / 1024,
+  });
+
+  const handleFormChange = (key: 'cpu' | 'memory', value: number) => {
+    setForm(v => ({ ...v, [key]: value }));
+  };
+  const checkedForm = useMemo(() => {
+    return formatForm(form);
+  }, [form]);
+  const { validation, checkIsValid } = useFormValidation(checkedForm);
+
+  const inputConfigs: ITextfieldConfig[] = useMemo(
+    () => [
+      {
+        label: `CPU (Core)`,
+        key: 'prometheus_address',
+        onChange: (v: string) => handleFormChange('cpu', +v),
+        variant: 'filled',
+        className: classes.input,
+        placeholder: 'CPU',
+        fullWidth: true,
+
+        defaultValue: form.cpu,
+      },
+      {
+        label: `Memory (GB)`,
+        key: 'prometheus_address',
+        onChange: (v: string) => handleFormChange('memory', +v),
+        variant: 'filled',
+        className: classes.input,
+        placeholder: 'Memory',
+        fullWidth: true,
+
+        defaultValue: form.memory,
+      },
+    ],
+    [form]
+  );
+
+  return (
+    <Dialog onClose={handleClose} open={open}>
+      <div className={classes.root}>
+        <div className={classes.note}>
+          {`Exceeding any threshold will result in a `}
+          <span
+            style={{
+              color: HEALTHY_STATUS_COLORS[EHealthyStatus.warning],
+              fontWeight: 600,
+              fontSize: 18,
+            }}
+          >
+            warning
+          </span>
+          .
+        </div>
+        {inputConfigs.map(v => (
+          <CustomInput
+            type="text"
+            textConfig={v}
+            key={v.label}
+            checkValid={checkIsValid}
+            validInfo={validation}
+          />
+        ))}
+        <div className={classes.button}>
+          <CustomButton variant="contained" onClick={handleClose}>
+            Confirm
+          </CustomButton>
+        </div>
+      </div>
+    </Dialog>
+  );
+}
+
+const ThresholdSetting = ({
+  threshold,
+  setThreshold,
+}: {
+  threshold: IThreshold;
+  setThreshold: (threshold: IThreshold) => void;
+}) => {
+  const [open, setOpen] = useState(false);
+
+  const handleClickOpen = () => {
+    setOpen(true);
+  };
+
+  const handleClose = () => {
+    setOpen(false);
+  };
+
+  return (
+    <>
+      <SettingsOutlinedIcon
+        onClick={handleClickOpen}
+        style={{
+          cursor: 'pointer',
+          opacity: 0.8,
+        }}
+      />
+      <ThresholdSettingDialog
+        threshold={threshold}
+        setThreshold={setThreshold}
+        open={open}
+        onClose={handleClose}
+      />
+    </>
+  );
+};
+
+export default ThresholdSetting;

+ 59 - 0
client/src/pages/systemHealthy/TimeRangeTabs.tsx

@@ -0,0 +1,59 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { Dispatch, Fragment, SetStateAction } from 'react';
+import { timeRangeOptions } from './consts';
+import { ITimeRangeOption } from './Types';
+import clsx from 'clsx';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    fontSize: '14px',
+    display: 'flex',
+    alignItems: 'flex-end',
+    color: '#999',
+    fontWeight: 500,
+  },
+  divider: {
+    margin: '0 4px',
+  },
+  label: {
+    cursor: 'pointer',
+    '&:hover': {
+      fontSize: '16px',
+    },
+  },
+  active: {
+    fontWeight: 600,
+    fontSize: '16px',
+    color: '#222',
+  },
+}));
+
+const TimeRangeTabs = ({
+  timeRange,
+  setTimeRange,
+}: {
+  timeRange: ITimeRangeOption;
+  setTimeRange: Dispatch<SetStateAction<ITimeRangeOption>>;
+}) => {
+  const classes = getStyles();
+  return (
+    <div className={classes.root}>
+      {timeRangeOptions.map((timeRangeOption, i: number) => (
+        <Fragment key={timeRangeOption.label}>
+          {i > 0 && <div className={classes.divider}>{'/'}</div>}
+          <div
+            className={clsx(
+              classes.label,
+              timeRangeOption.value === timeRange.value && classes.active
+            )}
+            onClick={() => setTimeRange(timeRangeOption)}
+          >
+            {timeRangeOption.label}
+          </div>
+        </Fragment>
+      ))}
+    </div>
+  );
+};
+
+export default TimeRangeTabs;

+ 296 - 0
client/src/pages/systemHealthy/Topology.tsx

@@ -0,0 +1,296 @@
+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;

+ 79 - 0
client/src/pages/systemHealthy/Types.ts

@@ -0,0 +1,79 @@
+export interface ITimeRangeOption {
+  label: string;
+  value: number;
+  step: number;
+}
+
+export enum ENodeType {
+  overview = 0,
+  coord = 'coord',
+  node = 'node',
+}
+
+export enum ENodeService {
+  milvus = 0,
+  meta = 'Meta',
+  msgstream = 'MsgStream',
+  objstorage = 'ObjStorage',
+  root = 'Root',
+  query = 'Query',
+  index = 'Index',
+  data = 'Data',
+}
+
+export interface ILineChartData {
+  label: string;
+  data: number[];
+  format?: (d: number) => string;
+  unit?: string;
+}
+
+export interface INodeTreeStructure {
+  service: ENodeService;
+  type: ENodeType;
+  label: string;
+  healthyStatus: EHealthyStatus[];
+  cpu?: number[];
+  memory?: number[];
+  children: INodeTreeStructure[];
+}
+
+export enum EPrometheusDataStatus {
+  noData = -1,
+  failed = -2,
+}
+
+export enum EHealthyStatus {
+  noData = 0,
+  healthy,
+  warning,
+  failed,
+}
+
+export interface IPrometheusNode {
+  type: string;
+  pod: string;
+  cpu: number[];
+  memory: number[];
+}
+
+export interface IPrometheusAllData {
+  totalVectorsCount: number[];
+  searchVectorsCount: number[];
+  searchFailedVectorsCount?: number[];
+  sqLatency: number[];
+
+  meta: number[];
+  msgstream: number[];
+  objstorage: number[];
+
+  rootNodes: IPrometheusNode[];
+  queryNodes: IPrometheusNode[];
+  indexNodes: IPrometheusNode[];
+  dataNodes: IPrometheusNode[];
+}
+
+export interface IThreshold {
+  cpu: number;
+  memory: number;
+}

+ 48 - 0
client/src/pages/systemHealthy/consts.ts

@@ -0,0 +1,48 @@
+import { EHealthyStatus, ITimeRangeOption } from './Types';
+
+export const TOPO_WIDTH = 600;
+export const TOPO_HEIGHT = 580;
+export const TOPO_NODE_R = [68, 45, 30];
+export const TOPO_LINK_LENGTH = [160, 270];
+
+export const MAIN_VIEW_WIDTH = 560;
+export const CHART_WIDTH = 450;
+export const HEALTHY_INDEX_ROW_HEIGHT = 20;
+export const HEALTHY_INDEX_ROW_GAP_RATIO = 0.3;
+export const HEALTHY_STATUS_COLORS = {
+  [EHealthyStatus.noData]: '#ccc',
+  [EHealthyStatus.healthy]: '#6CD676',
+  [EHealthyStatus.warning]: '#F4DD0E',
+  [EHealthyStatus.failed]: '#F16415',
+};
+
+export const LINE_CHART_LARGE_HEIGHT = 60;
+export const LINE_CHART_SMALL_HEIGHT = 48;
+// export const LINE_COLOR = '#394E97';
+export const LINE_COLOR = 'rgb(6, 175, 242)';
+export const LINE_WIDTH = 1;
+export const LINE_LABEL_Y_PADDING = 6;
+export const LINE_LABEL_FONT_SIZE = 14;
+export const LINE_SMALL_LABEL_FONT_SIZE = 12;
+export const timeRangeOptions: ITimeRangeOption[] = [
+  {
+    label: '1h',
+    value: 60 * 60 * 1000,
+    step: 3 * 60 * 1000,
+  },
+  {
+    label: '24h',
+    value: 24 * 60 * 60 * 1000,
+    step: 60 * 60 * 1000,
+  },
+  {
+    label: '7d',
+    value: 7 * 24 * 60 * 60 * 1000,
+    step: 8 * 60 * 60 * 1000,
+  },
+  {
+    label: '30d',
+    value: 30 * 24 * 60 * 60 * 1000,
+    step: 24 * 60 * 60 * 1000,
+  },
+];

+ 154 - 0
client/src/pages/systemHealthy/dataHandler.ts

@@ -0,0 +1,154 @@
+import * as d3 from 'd3';
+import {
+  EHealthyStatus,
+  ENodeService,
+  ENodeType,
+  EPrometheusDataStatus,
+  INodeTreeStructure,
+  IPrometheusAllData,
+  IPrometheusNode,
+  IThreshold,
+} from './Types';
+
+export const getInternalNode = (
+  prometheusNodes: IPrometheusNode[],
+  service: ENodeService,
+  label: string,
+  threshold: IThreshold
+) => {
+  const length = prometheusNodes[0].cpu.length;
+  const nodes = prometheusNodes
+    .map(node => {
+      const healthyStatus = d3.range(length).map((_, i: number) => {
+        const cpu = node.cpu[i];
+        const memory = node.memory[i];
+        if (cpu === EPrometheusDataStatus.noData) return EHealthyStatus.noData;
+        if (cpu === EPrometheusDataStatus.failed) return EHealthyStatus.failed;
+        return cpu >= threshold.cpu || memory >= threshold.memory
+          ? EHealthyStatus.warning
+          : EHealthyStatus.healthy;
+      });
+      return {
+        service: service,
+        type: node.type === 'coord' ? ENodeType.coord : ENodeType.node,
+        label: node.pod,
+        healthyStatus,
+        cpu: node.cpu,
+        memory: node.memory,
+        children: [],
+      };
+    })
+    .sort((a, b) => {
+      const failedCountA = a.healthyStatus.filter(
+        s => s === EHealthyStatus.failed
+      ).length;
+      const failedCountB = b.healthyStatus.filter(
+        s => s === EHealthyStatus.failed
+      ).length;
+      if (failedCountA !== failedCountB) return failedCountB - failedCountA;
+      const warningCountA = a.healthyStatus.filter(
+        s => s === EHealthyStatus.warning
+      ).length;
+      const warningCountB = b.healthyStatus.filter(
+        s => s === EHealthyStatus.warning
+      ).length;
+      return warningCountB - warningCountA;
+    });
+  const overviewHealthyStatus = d3.range(length).map((_, i: number) => {
+    if (nodes.find(node => node.healthyStatus[i] === EHealthyStatus.failed))
+      return EHealthyStatus.failed;
+    if (nodes.find(node => node.healthyStatus[i] === EHealthyStatus.warning))
+      return EHealthyStatus.warning;
+    if (nodes.find(node => node.healthyStatus[i] === EHealthyStatus.healthy))
+      return EHealthyStatus.healthy;
+    return EHealthyStatus.noData;
+  });
+  const overviewNode = {
+    service,
+    type: ENodeType.overview,
+    label: label,
+    healthyStatus: overviewHealthyStatus,
+    children: nodes,
+  };
+  return overviewNode;
+};
+
+export const reconNodeTree = (
+  prometheusData: IPrometheusAllData,
+  threshold: IThreshold
+) => {
+  // third party
+  const metaNode: INodeTreeStructure = {
+    service: ENodeService.meta,
+    type: ENodeType.overview,
+    label: 'Meta',
+    healthyStatus: rateList2healthyStatus(prometheusData.meta),
+    children: [],
+  };
+  const msgstreamNode: INodeTreeStructure = {
+    service: ENodeService.msgstream,
+    type: ENodeType.overview,
+    label: 'MsgStream',
+    healthyStatus: rateList2healthyStatus(prometheusData.msgstream),
+    children: [],
+  };
+  const objstorageNode: INodeTreeStructure = {
+    service: ENodeService.objstorage,
+    type: ENodeType.overview,
+    label: 'ObjStorage',
+    healthyStatus: rateList2healthyStatus(prometheusData.objstorage),
+    children: [],
+  };
+
+  // internal
+  const rootNode = getInternalNode(
+    prometheusData.rootNodes,
+    ENodeService.root,
+    'Root',
+    threshold
+  );
+  const indexNode = getInternalNode(
+    prometheusData.indexNodes,
+    ENodeService.index,
+    'Index',
+    threshold
+  );
+  const queryNode = getInternalNode(
+    prometheusData.queryNodes,
+    ENodeService.query,
+    'Query',
+    threshold
+  );
+  const dataNode = getInternalNode(
+    prometheusData.dataNodes,
+    ENodeService.data,
+    'Data',
+    threshold
+  );
+
+  return {
+    service: ENodeService.milvus,
+    type: ENodeType.overview,
+    label: 'Overview',
+    healthyStatus: [],
+    children: [
+      rootNode,
+      indexNode,
+      queryNode,
+      dataNode,
+      metaNode,
+      msgstreamNode,
+      objstorageNode,
+    ],
+  } as INodeTreeStructure;
+};
+
+export const THIRD_PARTY_SERVICE_HEALTHY_THRESHOLD = 0.95;
+export const getThirdPartyServiceHealthyStatus = (rate: number) => {
+  if (rate === -1) return EHealthyStatus.noData;
+  if (rate > THIRD_PARTY_SERVICE_HEALTHY_THRESHOLD)
+    return EHealthyStatus.healthy;
+  return EHealthyStatus.failed;
+};
+export const rateList2healthyStatus = (rateList: number[]) =>
+  rateList.map((rate: number) => getThirdPartyServiceHealthyStatus(rate));

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 15 - 0
client/src/pages/systemHealthy/getIcon.tsx


+ 198 - 0
client/src/pages/systemHealthy/prometheusDataCase.json

@@ -0,0 +1,198 @@
+{
+  "totalVectorsCount": [
+    10100, 10100, 10100, 10100, 10100, 10100, 20200, 20200, 20200, 20200, 20200,
+    20200, 20200, 20200, 20200, 20200, 20200, 20200, 20200, 20200, 20200
+  ],
+  "searchVectorsCount": [
+    51, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+  ],
+  "sqLatency": [
+    0, 1995.776, 0, 0, 509.44000000000005, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0
+  ],
+  "meta": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+  "msgstream": [
+    0.9999987192278481, 1.000001241850357, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1
+  ],
+  "objstorage": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+  "rootNodes": [
+    {
+      "type": "coord",
+      "pod": "tianmin-milvus-rootcoord-66bb975f6d-jxx6s",
+      "cpu": [
+        0.012685416666666671, 0.012543402777777778, 0.012973958333333353,
+        0.01245138888888887, 0.014343055555555553, 0.01412222222222223,
+        0.014353472222222226, 0.01409826388888888, 0.013789930555555574,
+        0.01384513888888888, 0.013060416666666647, 0.013064236111111112,
+        0.012952777777777808, 0.013211805555555555, 0.013215277777777791,
+        0.013023958333333275, 0.013395138888888911, 0.01334201388888889,
+        0.012905902777777796, 0.013392708333333303, 0.013308333333333356
+      ],
+      "memory": [
+        256786432, 256786432, 256163840, 256147456, 256143360, 256143360,
+        256139264, 234467328, 227409920, 226013184, 226013184, 226013184,
+        224165888, 224165888, 224063488, 224063488, 223633408, 223633408,
+        223633408, 223633408, 223592448
+      ]
+    }
+  ],
+  "queryNodes": [
+    {
+      "type": "coord",
+      "pod": "tianmin-milvus-querycoord-d779dfdd4-njjp5",
+      "cpu": [
+        0.004101041666666663, 0.003982291666666669, 0.004053125,
+        0.004022916666666671, 0.004190624999999994, 0.004188888888888892,
+        0.004214930555555551, 0.004066666666666663, 0.004054513888888888,
+        0.004250694444444447, 0.004367361111111118, 0.004352083333333338,
+        0.004473263888888886, 0.004397569444444448, 0.004364236111111097,
+        0.00445347222222223, 0.004335069444444441, 0.004356944444444445,
+        0.004137847222222225, 0.004144444444444449, 0.004073263888888887
+      ],
+      "memory": [
+        196358144, 199794688, 196919296, 196612096, 196096000, 195846144,
+        200585216, 199811072, 199643136, 200392704, 195579904, 197345280,
+        199356416, 199155712, 200818688, 201146368, 198569984, 199335936,
+        198352896, 201076736, 196235264
+      ]
+    },
+    {
+      "type": "node",
+      "pod": "tianmin-milvus-querynode-68866999f4-t8wtz",
+      "cpu": [
+        0.007112152777777775, 0.006854513888888884, 0.0073892361111111094,
+        0.006928472222222221, 0.008007291666666671, 0.008076736111111116,
+        0.008597916666666663, 0.008150694444444436, 0.007825694444444448,
+        0.00805312500000001, 0.007207638888888887, 0.007342013888888882,
+        0.00695937500000001, 0.007777430555555548, 0.0076819444444444366,
+        0.007389583333333355, 0.007785763888888873, 0.00781284722222223,
+        0.007146874999999998, 0.007248263888888889, 0.007239236111111103
+      ],
+      "memory": [
+        226652160, 224366592, 222994432, 227840000, 228249600, 226996224,
+        274440192, 251985920, 244887552, 256131072, 255901696, 255942656,
+        253829120, 255168512, 255004672, 261414912, 261603328, 261533696,
+        260169728, 260403200, 260911104
+      ]
+    },
+    {
+      "type": "node",
+      "pod": "tianmin-milvus-querynode-68866999f4-ftmql",
+      "cpu": [
+        0.006090277777777781, 0.0058100694444444415, 0.006048611111111121,
+        0.005899652777777773, 0.006252430555555561, 0.0062645833333333355,
+        0.006651388888888887, 0.0061711805555555565, 0.005942708333333336,
+        0.006573958333333331, 0.0064229166666666514, 0.006414583333333357,
+        0.006329513888888888, 0.006902083333333324, 0.006721180555555545,
+        0.00663472222222222, 0.006575694444444448, 0.006790277777777792,
+        0.006086458333333332, 0.006110763888888882, 0.005955902777777769
+      ],
+      "memory": [
+        241979392, 237637632, 237838336, 245350400, 245428224, 244047872,
+        294604800, 293490688, 293425152, 293871616, 293416960, 293269504,
+        293273600, 294273024, 294141952, 294559744, 294559744, 294158336,
+        293429248, 293548032, 293933056
+      ]
+    }
+  ],
+  "indexNodes": [
+    {
+      "type": "coord",
+      "pod": "tianmin-milvus-indexcoord-864d49b47f-t7w4k",
+      "cpu": [
+        0.0009246527777777776, 0.0009281249999999986, 0.0009041666666666674,
+        0.0009208333333333327, 0.0009611111111111114, 0.0009899305555555553,
+        0.0009916666666666687, 0.0009406249999999971, 0.0009343750000000028,
+        0.0009451388888888859, 0.0009118055555555591, 0.0009263888888888872,
+        0.0009142361111111125, 0.0009312499999999978, 0.0009114583333333333,
+        0.0009156250000000002, 0.0009107638888888896, 0.0009145833333333344,
+        0.001040624999999997, 0.0010041666666666693, 0.0010395833333333314
+      ],
+      "memory": [
+        179036160, 179437568, 178999296, 179400704, 179265536, 179404800,
+        179617792, 179322880, 179195904, 179769344, 179130368, 179163136,
+        179671040, 179466240, 179105792, 179281920, 179163136, 179449856,
+        179531776, 179519488, 179597312
+      ]
+    },
+    {
+      "type": "node",
+      "pod": "tianmin-milvus-indexnode-6cdc5f745b-j5ttn",
+      "cpu": [
+        0.0006562499999999992, 0.0006225694444444447, 0.000621180555555555,
+        0.000625, 0.0006256944444444458, 0.0006343749999999994,
+        0.0008763888888888892, 0.0006246527777777781, 0.0006236111111111104,
+        0.0006934027777777787, 0.0007628472222222212, 0.0007402777777777775,
+        0.0007593750000000002, 0.0007652777777777785, 0.0007510416666666665,
+        0.0007649305555555546, 0.0007270833333333352, 0.0007420138888888891,
+        0.0006767361111111115, 0.0006892361111111079, 0.0006680555555555559
+      ],
+      "memory": [
+        1075818496, 1075908608, 1076088832, 1075875840, 1076109312, 1076006912,
+        1401651200, 1401458688, 1401626624, 1401880576, 1401569280, 1401815040,
+        1401688064, 1401462784, 1401683968, 1401614336, 1401364480, 1401761792,
+        1401597952, 1401376768, 1401483264
+      ]
+    }
+  ],
+  "dataNodes": [
+    {
+      "type": "coord",
+      "pod": "tianmin-milvus-datacoord-bb57486b-w29cx",
+      "cpu": [
+        0.0034170138888888917, 0.0033708333333333307, 0.003458333333333338,
+        0.003440277777777775, 0.0035854166666666664, 0.0036586805555555517,
+        0.003824305555555559, 0.0035819444444444393, 0.0035041666666666693,
+        0.003850694444444448, 0.0038048611111111087, 0.0037906250000000023,
+        0.0037343750000000064, 0.004230902777777774, 0.004072916666666657,
+        0.0041246527777777766, 0.00406284722222223, 0.004109374999999997,
+        0.003622916666666672, 0.0035673611111111036, 0.003509027777777776
+      ],
+      "memory": [
+        207458304, 205180928, 205942784, 203255808, 205172736, 204587008,
+        204673024, 207888384, 207896576, 208162816, 206340096, 207425536,
+        207859712, 208457728, 207384576, 206680064, 207507456, 204734464,
+        206397440, 206385152, 204709888
+      ]
+    },
+    {
+      "type": "node",
+      "pod": "tianmin-milvus-datanode-7b759b9697-snqqx",
+      "cpu": [
+        0.005009374999999999, 0.005132291666666664, 0.005037152777777784,
+        0.0051555555555555565, 0.005372916666666659, 0.005334722222222218,
+        0.005506250000000013, 0.005367013888888879, 0.005113541666666666,
+        0.005218402777777777, 0.005051388888888889, 0.00507847222222223,
+        0.0049961805555555515, 0.0053750000000000065, 0.005430902777777789,
+        0.005427083333333308, 0.005367013888888911, 0.005399305555555556,
+        0.005404861111111106, 0.0053663194444444505, 0.005429861111111083
+      ],
+      "memory": [
+        233619456, 232636416, 232497152, 232665088, 232919040, 232497152,
+        260325376, 259923968, 260128768, 266887168, 266567680, 266571776,
+        266575872, 267214848, 267214848, 267214848, 267223040, 267223040,
+        266584064, 266481664, 266694656
+      ]
+    },
+    {
+      "type": "node",
+      "pod": "tianmin-milvus-datanode-7b759b9697-hzd9w",
+      "cpu": [
+        0.005274652777777773, 0.0052645833333333295, 0.005237500000000005,
+        0.005409027777777784, 0.00546284722222222, 0.0052670138888888905,
+        0.005463541666666663, 0.0053881944444444385, 0.005268055555555564,
+        0.00593715277777777, 0.006545833333333332, 0.006434374999999998,
+        0.006494444444444443, 0.006991666666666687, 0.006936111111111087,
+        0.007105902777777797, 0.006855208333333312, 0.006859375000000006,
+        0.005905555555555553, 0.005819791666666687, 0.0057913194444444436
+      ],
+      "memory": [
+        223993856, 223170560, 223100928, 223793152, 223834112, 222822400,
+        257933312, 257347584, 257466368, 257581056, 257581056, 257318912,
+        257318912, 257642496, 257642496, 257642496, 257966080, 258170880,
+        257781760, 257511424, 257298432
+      ]
+    }
+  ]
+}

+ 5 - 0
client/src/router/Router.tsx

@@ -6,6 +6,7 @@ import Users from '../pages/user/User';
 import Index from '../pages/index';
 import Search from '../pages/search/VectorSearch';
 import System from '../pages/system/SystemView';
+import SystemHealthy from '../pages/systemHealthy/SystemHealthyView';
 
 const router = createHashRouter([
   {
@@ -32,6 +33,10 @@ const router = createHashRouter([
         path: '/system',
         element: <System />,
       },
+      {
+        path: '/system_healthy',
+        element: <SystemHealthy />,
+      },
     ],
   },
   { path: '/connect', element: <Connect /> },

+ 13 - 1
client/src/utils/Format.ts

@@ -1,5 +1,5 @@
 import { BYTE_UNITS } from '../consts/Util';
-import { DEFAULT_MILVUS_PORT } from '../consts/Milvus';
+import { DEFAULT_MILVUS_PORT, DEFAULT_PROMETHEUS_PORT } from '../consts/Milvus';
 import {
   CreateFieldType,
   DataTypeEnum,
@@ -148,6 +148,18 @@ export const formatAddress = (address: string): string => {
   return ip.includes(':') ? ip : `${ip}:${DEFAULT_MILVUS_PORT}`;
 };
 
+// format the prometheus address
+export const formatPrometheusAddress = (address: string): string => {
+  let formatAddress = address;
+  // add protocal (default http)
+  const withProtocol = address.includes('http');
+  if (!withProtocol) formatAddress = 'http://' + formatAddress;
+  // add port (default 9090)
+  const withPort = address.includes(':');
+  if (!withPort) formatAddress = formatAddress + ':' + DEFAULT_PROMETHEUS_PORT;
+  return formatAddress;
+};
+
 export const formatByteSize = (
   size: number,
   capacityTrans: { [key in string]: string }

+ 487 - 1
client/yarn.lock

@@ -872,11 +872,226 @@
   resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.3.tgz#3c90752792660c4b562ad73b3fbd68bf3bc7ae07"
   integrity sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==
 
+"@types/d3-array@*":
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.0.4.tgz#44eebe40be57476cad6a0cd6a85b0f57d54185a2"
+  integrity sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ==
+
+"@types/d3-axis@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.2.tgz#96e11d51256baf5bdb2fa73a17d302993e79df07"
+  integrity sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-brush@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.2.tgz#a610aad5a1e76c375be63e11c5eee1ed9fd2fb40"
+  integrity sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-chord@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.2.tgz#cf6f05ad2d8faaad524e9e6f454b4fd06b200930"
+  integrity sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw==
+
+"@types/d3-color@*":
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4"
+  integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==
+
+"@types/d3-contour@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.2.tgz#d8a0e4d12ec14f7d2bb6e59f3fbc1a527457d0b2"
+  integrity sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ==
+  dependencies:
+    "@types/d3-array" "*"
+    "@types/geojson" "*"
+
+"@types/d3-delaunay@*":
+  version "6.0.1"
+  resolved "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz#006b7bd838baec1511270cb900bf4fc377bbbf41"
+  integrity sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==
+
+"@types/d3-dispatch@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz#b2fa80bab3bcead68680766e966f59cd6cb9a69f"
+  integrity sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg==
+
+"@types/d3-drag@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.2.tgz#5562da3e7b33d782c2c1f9e65c5e91bb01ee82cf"
+  integrity sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-dsv@*":
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.1.tgz#c51a3505cee42653454b74a00f8713dc3548c362"
+  integrity sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw==
+
+"@types/d3-ease@*":
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0"
+  integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==
+
+"@types/d3-fetch@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.2.tgz#fe1f335243e07c9bd520c9a71756fed8330c54b1"
+  integrity sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA==
+  dependencies:
+    "@types/d3-dsv" "*"
+
+"@types/d3-force@*":
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.4.tgz#2d50bd2b695f709797e1745644f6bc123e6e5f5a"
+  integrity sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==
+
+"@types/d3-format@*":
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d"
+  integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==
+
+"@types/d3-geo@*":
+  version "3.0.3"
+  resolved "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.0.3.tgz#535e5f24be13722964c52354301be09b752f5d6e"
+  integrity sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/d3-hierarchy@*":
+  version "3.1.2"
+  resolved "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b3a446b5437faededb30ac32b7cc0486559ab1e2"
+  integrity sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A==
+
+"@types/d3-interpolate@*":
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
+  integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==
+  dependencies:
+    "@types/d3-color" "*"
+
+"@types/d3-path@*":
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b"
+  integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==
+
+"@types/d3-polygon@*":
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93"
+  integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==
+
+"@types/d3-quadtree@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5"
+  integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==
+
+"@types/d3-random@*":
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953"
+  integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==
+
+"@types/d3-scale-chromatic@*":
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954"
+  integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==
+
+"@types/d3-scale@*":
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5"
+  integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==
+  dependencies:
+    "@types/d3-time" "*"
+
+"@types/d3-selection@*":
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.4.tgz#923d7f8985718116de56f55307d26e5f00728dc5"
+  integrity sha512-ZeykX7286BCyMg9sH5fIAORyCB6hcATPSRQpN47jwBA2bMbAT0s+EvtDP5r1FZYJ95R8QoEE1CKJX+n0/M5Vhg==
+
+"@types/d3-shape@*":
+  version "3.1.1"
+  resolved "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.1.tgz#15cc497751dac31192d7aef4e67a8d2c62354b95"
+  integrity sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==
+  dependencies:
+    "@types/d3-path" "*"
+
+"@types/d3-time-format@*":
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946"
+  integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==
+
+"@types/d3-time@*":
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819"
+  integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==
+
+"@types/d3-timer@*":
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce"
+  integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==
+
+"@types/d3-transition@*":
+  version "3.0.3"
+  resolved "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.3.tgz#d4ac37d08703fb039c87f92851a598ba77400402"
+  integrity sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-zoom@*":
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.2.tgz#067aa6a6ecbc75a78b753cc6f7a7f9f7e4e7d117"
+  integrity sha512-t09DDJVBI6AkM7N8kuPsnq/3d/ehtRKBN1xSiYjjMCgbiw6HM6Ged5VhvswmhprfKyGvzeTEL/4WBaK9llWvlA==
+  dependencies:
+    "@types/d3-interpolate" "*"
+    "@types/d3-selection" "*"
+
+"@types/d3@^7.4.0":
+  version "7.4.0"
+  resolved "https://registry.npmmirror.com/@types/d3/-/d3-7.4.0.tgz#fc5cac5b1756fc592a3cf1f3dc881bf08225f515"
+  integrity sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==
+  dependencies:
+    "@types/d3-array" "*"
+    "@types/d3-axis" "*"
+    "@types/d3-brush" "*"
+    "@types/d3-chord" "*"
+    "@types/d3-color" "*"
+    "@types/d3-contour" "*"
+    "@types/d3-delaunay" "*"
+    "@types/d3-dispatch" "*"
+    "@types/d3-drag" "*"
+    "@types/d3-dsv" "*"
+    "@types/d3-ease" "*"
+    "@types/d3-fetch" "*"
+    "@types/d3-force" "*"
+    "@types/d3-format" "*"
+    "@types/d3-geo" "*"
+    "@types/d3-hierarchy" "*"
+    "@types/d3-interpolate" "*"
+    "@types/d3-path" "*"
+    "@types/d3-polygon" "*"
+    "@types/d3-quadtree" "*"
+    "@types/d3-random" "*"
+    "@types/d3-scale" "*"
+    "@types/d3-scale-chromatic" "*"
+    "@types/d3-selection" "*"
+    "@types/d3-shape" "*"
+    "@types/d3-time" "*"
+    "@types/d3-time-format" "*"
+    "@types/d3-timer" "*"
+    "@types/d3-transition" "*"
+    "@types/d3-zoom" "*"
+
 "@types/file-saver@^2.0.4":
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.4.tgz#aaf9b96296150d737b2fefa535ced05ed8013d84"
   integrity sha512-sPZYQEIF/SOnLAvaz9lTuydniP+afBMtElRTdYkeV1QtEgvtJ7qolCPjly6O32QI8CbEmP5O/fztMXEDWfEcrg==
 
+"@types/geojson@*":
+  version "7946.0.10"
+  resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
+  integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==
+
 "@types/hast@^2.0.0":
   version "2.3.4"
   resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
@@ -1410,6 +1625,11 @@ comma-separated-tokens@^1.0.0:
   resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
   integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
 
+commander@7:
+  version "7.2.0"
+  resolved "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -1487,6 +1707,250 @@ csstype@^3.0.2:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b"
   integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==
 
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
+  version "3.2.2"
+  resolved "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.2.tgz#f8ac4705c5b06914a7e0025bbf8d5f1513f6a86e"
+  integrity sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==
+  dependencies:
+    internmap "1 - 2"
+
+d3-axis@3:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
+  integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
+  integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "3"
+    d3-transition "3"
+
+d3-chord@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
+  integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+  dependencies:
+    d3-path "1 - 3"
+
+"d3-color@1 - 3", d3-color@3:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+  integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+  version "4.0.2"
+  resolved "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
+  integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+  dependencies:
+    d3-array "^3.2.0"
+
+d3-delaunay@6:
+  version "6.0.2"
+  resolved "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92"
+  integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==
+  dependencies:
+    delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+  integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+  integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
+  integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+  dependencies:
+    commander "7"
+    iconv-lite "0.6"
+    rw "1"
+
+"d3-ease@1 - 3", d3-ease@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+  integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
+  integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+  dependencies:
+    d3-dsv "1 - 3"
+
+d3-force@3:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+  integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-quadtree "1 - 3"
+    d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+  integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-geo@3:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
+  integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
+  dependencies:
+    d3-array "2.5.0 - 3"
+
+d3-hierarchy@3:
+  version "3.1.2"
+  resolved "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
+  integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+  integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+  dependencies:
+    d3-color "1 - 3"
+
+"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+  integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-polygon@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
+  integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+  integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
+  integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-scale-chromatic@3:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
+  integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
+  dependencies:
+    d3-color "1 - 3"
+    d3-interpolate "1 - 3"
+
+d3-scale@4:
+  version "4.0.2"
+  resolved "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+  integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+  dependencies:
+    d3-array "2.10.0 - 3"
+    d3-format "1 - 3"
+    d3-interpolate "1.2.0 - 3"
+    d3-time "2.1.1 - 3"
+    d3-time-format "2 - 4"
+
+"d3-selection@2 - 3", d3-selection@3:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+  integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@3:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+  integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+  dependencies:
+    d3-path "^3.1.0"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+  version "4.1.0"
+  resolved "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+  integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+  dependencies:
+    d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+  integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+  dependencies:
+    d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+  integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3", d3-transition@3:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+  integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+  dependencies:
+    d3-color "1 - 3"
+    d3-dispatch "1 - 3"
+    d3-ease "1 - 3"
+    d3-interpolate "1 - 3"
+    d3-timer "1 - 3"
+
+d3-zoom@3:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+  integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "2 - 3"
+    d3-transition "2 - 3"
+
+d3@^7.8.2:
+  version "7.8.2"
+  resolved "https://registry.npmmirror.com/d3/-/d3-7.8.2.tgz#2bdb3c178d095ae03b107a18837ae049838e372d"
+  integrity sha512-WXty7qOGSHb7HR7CfOzwN1Gw04MUOzN8qh9ZUsvwycIMb4DYMpY9xczZ6jUorGtO6bR9BPMPaueIKwiDxu9uiQ==
+  dependencies:
+    d3-array "3"
+    d3-axis "3"
+    d3-brush "3"
+    d3-chord "3"
+    d3-color "3"
+    d3-contour "4"
+    d3-delaunay "6"
+    d3-dispatch "3"
+    d3-drag "3"
+    d3-dsv "3"
+    d3-ease "3"
+    d3-fetch "3"
+    d3-force "3"
+    d3-format "3"
+    d3-geo "3"
+    d3-hierarchy "3"
+    d3-interpolate "3"
+    d3-path "3"
+    d3-polygon "3"
+    d3-quadtree "3"
+    d3-random "3"
+    d3-scale "4"
+    d3-scale-chromatic "3"
+    d3-selection "3"
+    d3-shape "3"
+    d3-time "3"
+    d3-time-format "4"
+    d3-timer "3"
+    d3-transition "3"
+    d3-zoom "3"
+
 data-urls@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
@@ -1561,6 +2025,13 @@ define-properties@^1.1.3, define-properties@^1.1.4:
     has-property-descriptors "^1.0.0"
     object-keys "^1.1.1"
 
+delaunator@5:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
+  integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
+  dependencies:
+    robust-predicates "^3.0.0"
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -2111,7 +2582,7 @@ i18next@^20.3.1:
   dependencies:
     "@babel/runtime" "^7.12.0"
 
-iconv-lite@0.6.3:
+iconv-lite@0.6, iconv-lite@0.6.3:
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
   integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -2144,6 +2615,11 @@ inherits@2:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
+"internmap@1 - 2":
+  version "2.0.3"
+  resolved "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+  integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
 is-alphabetical@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
@@ -3062,6 +3538,11 @@ rimraf@^3.0.2:
   dependencies:
     glob "^7.1.3"
 
+robust-predicates@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
+  integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
+
 rollup@^2.79.1:
   version "2.79.1"
   resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
@@ -3069,6 +3550,11 @@ rollup@^2.79.1:
   optionalDependencies:
     fsevents "~2.3.2"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+  integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
 safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"

+ 1 - 0
server/package.json

@@ -13,6 +13,7 @@
   },
   "dependencies": {
     "@zilliz/milvus2-sdk-node": "^2.2.1",
+    "axios": "^1.3.2",
     "chalk": "^4.1.2",
     "class-sanitizer": "^1.0.1",
     "class-transformer": "^0.4.0",

+ 2 - 0
server/src/app.ts

@@ -13,6 +13,7 @@ import { router as partitionsRouter } from './partitions';
 import { router as schemaRouter } from './schema';
 import { router as cronsRouter } from './crons';
 import { router as userRouter } from './users';
+import { router as prometheusRouter } from './prometheus';
 import { pubSub } from './events';
 import {
   TransformResMiddlerware,
@@ -41,6 +42,7 @@ router.use('/partitions', partitionsRouter);
 router.use('/schema', schemaRouter);
 router.use('/crons', cronsRouter);
 router.use('/users', userRouter);
+router.use('/prometheus', prometheusRouter);
 router.get('/healthy', (req, res, next) => {
   res.json({ status: 200 });
   next();

+ 0 - 0
server/src/prometheus/dto.ts


+ 34 - 0
server/src/prometheus/fillRangeData.ts

@@ -0,0 +1,34 @@
+export enum EPrometheusDataStatus {
+  noData = -1,
+  failed = -2,
+}
+
+export const fillRangeData = (
+  items: any[],
+  start: number,
+  end: number,
+  step: number
+) => {
+  const length = Math.floor((+end - start) / step) + 1;
+  if (length >= 0) {
+    const timeRange = Array(length)
+      .fill(0)
+      .map((_, i) => +start + i * step)
+      .map(d => d / 1000);
+
+    items.forEach(item => {
+      const dict = {} as any;
+      item.values.forEach(([t, v]: any) => (dict[t] = isNaN(v) ? 0 : +v));
+      const minTime = Math.min(...item.values.map((d: any) => d[0]));
+      item.values = timeRange.map(t =>
+        t in dict
+          ? dict[t]
+          : t > minTime
+          ? EPrometheusDataStatus.failed
+          : EPrometheusDataStatus.noData
+      );
+    });
+  }
+};
+
+export default fillRangeData;

+ 6 - 0
server/src/prometheus/index.ts

@@ -0,0 +1,6 @@
+import { PrometheusController } from './prometheus.controller';
+
+const prometheusManager = new PrometheusController();
+const router = prometheusManager.generateRoutes();
+
+export { router };

+ 46 - 0
server/src/prometheus/prometheus.controller.ts

@@ -0,0 +1,46 @@
+import { NextFunction, Request, Response, Router } from 'express';
+import { PrometheusService } from './prometheus.service';
+
+export class PrometheusController {
+  private router: Router;
+  private prometheusService: PrometheusService;
+
+  constructor() {
+    this.prometheusService = new PrometheusService();
+    this.router = Router();
+  }
+
+  generateRoutes() {
+    this.router.get('/setPrometheus', this.setPrometheus.bind(this));
+    this.router.get(
+      '/getMilvusHealthyData',
+      this.getMilvusHealthyData.bind(this)
+    );
+
+    return this.router;
+  }
+
+  async setPrometheus(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.prometheusService.setPrometheus(
+        req.query as any
+      );
+      res.send(result);
+    } catch (err) {
+      console.error(err);
+      next(err);
+    }
+  }
+
+  async getMilvusHealthyData(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.prometheusService.getMilvusHealthyData(
+        req.query as any
+      );
+      res.send(result);
+    } catch (err) {
+      console.error(err);
+      next(err);
+    }
+  }
+}

+ 280 - 0
server/src/prometheus/prometheus.service.ts

@@ -0,0 +1,280 @@
+import axios from 'axios';
+import fillRangeData from './fillRangeData';
+
+interface IPrometheusNode {
+  type: string;
+  pod: string;
+  cpu: number[];
+  memory: number[];
+}
+
+interface IPrometheusAllData {
+  totalVectorsCount: number[];
+  searchVectorsCount: number[];
+  searchFailedVectorsCount?: number[];
+  sqLatency: number[];
+
+  meta: number[];
+  msgstream: number[];
+  objstorage: number[];
+
+  rootNodes: IPrometheusNode[];
+  queryNodes: IPrometheusNode[];
+  indexNodes: IPrometheusNode[];
+  dataNodes: IPrometheusNode[];
+}
+
+const metaMetric = 'milvus_meta_op_count';
+const msgstreamMetric = 'milvus_msgstream_op_count';
+const objstorageMetric = 'milvus_storage_op_count';
+
+const totalVectorsCountMetric = 'milvus_proxy_insert_vectors_count';
+const searchVectorsCountMetric = 'milvus_proxy_search_vectors_count';
+const sqLatencyMetric = 'milvus_proxy_sq_latency_bucket';
+
+const cpuMetric = 'process_cpu_seconds_total';
+const memoryMetric = 'process_resident_memory_bytes';
+
+const http = axios.create({
+  timeout: 1000,
+});
+
+export class PrometheusService {
+  static address: string = '';
+  static instance: string = '';
+  static namespace: string = '';
+  static isReady: boolean = false;
+
+  static get selector() {
+    return (
+      '{' +
+      `app_kubernetes_io_instance="${PrometheusService.instance}"` +
+      `,namespace="${PrometheusService.namespace}"` +
+      '}'
+    );
+  }
+
+  constructor() {
+    // todo
+  }
+
+  async setPrometheus({
+    prometheusAddress,
+    prometheusInstance,
+    prometheusNamespace,
+  }: {
+    prometheusAddress: string;
+    prometheusInstance: string;
+    prometheusNamespace: string;
+  }) {
+    PrometheusService.isReady = await this.checkPrometheus(
+      prometheusAddress,
+      prometheusInstance,
+      prometheusNamespace
+    );
+    if (PrometheusService.isReady) {
+      PrometheusService.address = prometheusAddress;
+      PrometheusService.instance = prometheusInstance;
+      PrometheusService.namespace = prometheusNamespace;
+    }
+
+    return {
+      isReady: PrometheusService.isReady,
+    };
+  }
+
+  async checkPrometheus(
+    prometheusAddress: string,
+    prometheusInstance: string,
+    prometheusNamespace: string
+  ) {
+    const addressValid = await http
+      .get(`${prometheusAddress}/-/ready`)
+      .then(res => res?.status === 200)
+      .catch(err => {
+        return false;
+      });
+    if (addressValid) {
+      const url =
+        `${prometheusAddress}/api/v1/query` +
+        `?query=milvus_num_node{` +
+        `app_kubernetes_io_instance="${prometheusInstance}",` +
+        `namespace="${prometheusNamespace}"}`;
+      const instanceValid = await http
+        .get(url)
+        .then(res => res?.data?.data?.result?.length > 0)
+        .catch(err => {
+          return false;
+        });
+      if (instanceValid) return true;
+    }
+    return false;
+  }
+
+  async queryRange(expr: string, start: number, end: number, step: number) {
+    const url =
+      PrometheusService.address +
+      '/api/v1/query_range?query=' +
+      expr +
+      `&start=${new Date(+start).toISOString()}` +
+      `&end=${new Date(+end).toISOString()}` +
+      `&step=${step / 1000}s`;
+    const result = await http.get(url).then(res => res.data);
+    const data = result.data.result;
+    fillRangeData(data, start, end, step);
+    return data;
+  }
+
+  async getInsertVectorsCount(start: number, end: number, step: number) {
+    const expr = `${totalVectorsCountMetric}${PrometheusService.selector}`;
+    const data = await this.queryRange(expr, start, end, step);
+    return data.length > 0 ? data[0].values : [];
+  }
+
+  async getSearchVectorsCount(start: number, end: number, step: number) {
+    const expr = `delta(${searchVectorsCountMetric}${
+      PrometheusService.selector
+    }[${step / 1000}s])`;
+    const data = await this.queryRange(expr, start, end, step);
+    return data.length > 0
+      ? data[0].values.map((d: number) => Math.round(d))
+      : [];
+  }
+
+  async getSQLatency(start: number, end: number, step: number) {
+    const expr =
+      `histogram_quantile(0.99, sum by (le, pod, node_id)` +
+      `(rate(${sqLatencyMetric}${PrometheusService.selector}[${
+        step / 1000
+      }s])))`;
+    const data = await this.queryRange(expr, start, end, step);
+    return data.length > 0 ? data[0].values : [];
+  }
+
+  async getThirdPartyServiceHealthStatus(
+    metricName: string,
+    start: number,
+    end: number,
+    step: number
+  ) {
+    const expr = `sum by (status) (delta(${metricName}${
+      PrometheusService.selector
+    }[${step / 1000}s]))`;
+    const data = await this.queryRange(expr, start, end, step);
+
+    const totalData =
+      data.find((d: any) => d.metric.status === 'total')?.values || [];
+    const successData =
+      data.find((d: any) => d.metric.status === 'success')?.values || [];
+    return totalData.map((d: number, i: number) =>
+      d < 0 ? d : d === 0 ? 1 : successData[i] / d
+    );
+  }
+
+  async getInternalNodesCPUData(start: number, end: number, step: number) {
+    const expr = `rate(${cpuMetric}${PrometheusService.selector}[${
+      step / 1000
+    }s])`;
+    return await this.queryRange(expr, start, end, step);
+  }
+
+  async getInternalNodesMemoryData(start: number, end: number, step: number) {
+    const expr = `${memoryMetric}${PrometheusService.selector}`;
+    return await this.queryRange(expr, start, end, step);
+  }
+
+  reconstructNodeData(
+    cpuNodesData: any,
+    memoryNodesData: any,
+    type: string
+  ): IPrometheusNode[] {
+    const cpuNodes = cpuNodesData.filter(
+      (d: any) => d.metric.container.indexOf(type) >= 0
+    );
+    const memoryNodes = memoryNodesData.filter(
+      (d: any) => d.metric.container.indexOf(type) >= 0
+    );
+    const nodesData = cpuNodes.map((d: any) => {
+      const nodeType =
+        d.metric.container.indexOf('coord') >= 0 ? 'coord' : 'node';
+      const pod = d.metric.pod;
+      const cpu = d?.values || [];
+
+      const node = memoryNodes.find((data: any) => data.metric.pod === pod);
+      const memory = node ? node?.values || [] : [];
+
+      return {
+        type: nodeType,
+        pod,
+        cpu,
+        memory,
+      } as IPrometheusNode;
+    });
+
+    return nodesData;
+  }
+
+  async getInternalNodesData(start: number, end: number, step: number) {
+    const [cpuNodes, memoryNodes] = await Promise.all([
+      this.getInternalNodesCPUData(start, end, step),
+      this.getInternalNodesMemoryData(start, end, step),
+    ]);
+
+    const [rootNodes, queryNodes, indexNodes, dataNodes] = [
+      'root',
+      'query',
+      'index',
+      'data',
+    ].map((metric: string) =>
+      this.reconstructNodeData(cpuNodes, memoryNodes, metric)
+    );
+    return { rootNodes, queryNodes, indexNodes, dataNodes };
+  }
+
+  async getMilvusHealthyData({
+    start,
+    end,
+    step,
+  }: {
+    start: number;
+    end: number;
+    step: number;
+  }) {
+    if (!PrometheusService.isReady) {
+      return {};
+    }
+
+    const [
+      meta,
+      msgstream,
+      objstorage,
+      totalVectorsCount,
+      searchVectorsCount,
+      sqLatency,
+      { rootNodes, queryNodes, indexNodes, dataNodes },
+    ] = await Promise.all([
+      this.getThirdPartyServiceHealthStatus(metaMetric, start, end, step),
+      this.getThirdPartyServiceHealthStatus(msgstreamMetric, start, end, step),
+      this.getThirdPartyServiceHealthStatus(objstorageMetric, start, end, step),
+      this.getInsertVectorsCount(start, end, step),
+      this.getSearchVectorsCount(start, end, step),
+      this.getSQLatency(start, end, step),
+      this.getInternalNodesData(start, end, step),
+    ]);
+
+    return {
+      totalVectorsCount,
+      searchVectorsCount,
+      sqLatency,
+
+      meta,
+      msgstream,
+      objstorage,
+
+      rootNodes,
+      queryNodes,
+      indexNodes,
+      dataNodes,
+    } as IPrometheusAllData;
+  }
+}

+ 19 - 0
server/yarn.lock

@@ -1390,6 +1390,15 @@ at-least-node@^1.0.0:
   resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
+axios@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.npmmirror.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3"
+  integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==
+  dependencies:
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
 babel-jest@^27.3.1:
   version "27.3.1"
   resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.3.1.tgz#0636a3404c68e07001e434ac4956d82da8a80022"
@@ -2580,6 +2589,11 @@ find-up@^4.0.0, find-up@^4.1.0:
     locate-path "^5.0.0"
     path-exists "^4.0.0"
 
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
 form-data@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
@@ -4360,6 +4374,11 @@ proxy-addr@~2.0.5:
     forwarded "0.2.0"
     ipaddr.js "1.9.1"
 
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
 psl@^1.1.33:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác