Browse Source

Merge pull request #160 from zilliztech/add-replica

Add replica support
ryjiang 2 years ago
parent
commit
10a1a58138

+ 7 - 4
client/src/components/customTabList/CustomTabList.tsx

@@ -4,6 +4,10 @@ import { ITabListProps, ITabPanel } from './Types';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
+    display: 'flex',
+    flexDirection: 'column',
+    flexBasis: 0,
+    flexGrow: 1,
     '& .MuiTab-wrapper': {
     '& .MuiTab-wrapper': {
       textTransform: 'capitalize',
       textTransform: 'capitalize',
       fontWeight: 'bold',
       fontWeight: 'bold',
@@ -25,7 +29,7 @@ const useStyles = makeStyles((theme: Theme) => ({
     flexBasis: 0,
     flexBasis: 0,
     flexGrow: 1,
     flexGrow: 1,
     marginTop: theme.spacing(2),
     marginTop: theme.spacing(2),
-    overflowY: 'auto',
+    overflow: 'hidden',
   },
   },
 }));
 }));
 
 
@@ -65,10 +69,9 @@ const CustomTabList: FC<ITabListProps> = props => {
   };
   };
 
 
   return (
   return (
-    <>
+    <div className={`${classes.wrapper}  ${wrapperClass}`}>
       <Tabs
       <Tabs
         classes={{
         classes={{
-          root: `${classes.wrapper} ${wrapperClass}`,
           indicator: classes.tab,
           indicator: classes.tab,
           flexContainer: classes.tabContainer,
           flexContainer: classes.tabContainer,
         }}
         }}
@@ -99,7 +102,7 @@ const CustomTabList: FC<ITabListProps> = props => {
           {tab.component}
           {tab.component}
         </TabPanel>
         </TabPanel>
       ))}
       ))}
-    </>
+    </div>
   );
   );
 };
 };
 
 

+ 196 - 0
client/src/components/dialogs/LoadCollectionDialog.tsx

@@ -0,0 +1,196 @@
+import { useEffect, useState, useContext, useMemo } from 'react';
+import {
+  Typography,
+  makeStyles,
+  Theme,
+  Switch,
+  FormControlLabel,
+} from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { CollectionHttp } from '../../http/Collection';
+import { rootContext } from '../../context/Root';
+import { useFormValidation } from '../../hooks/Form';
+import { formatForm } from '../../utils/Form';
+import { parseJson, getNode } from '../../utils/Metric';
+import CustomInput from '../customInput/CustomInput';
+import { ITextfieldConfig } from '../customInput/Types';
+import DialogTemplate from '../customDialog/DialogTemplate';
+import { MilvusHttp } from '../../http/Milvus';
+import CustomToolTip from '../customToolTip/CustomToolTip';
+import { MILVUS_NODE_TYPE, MILVUS_DEPLOY_MODE } from '../../consts/Milvus';
+import icons from '../icons/Icons';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  desc: {
+    marginBottom: theme.spacing(2),
+    maxWidth: 480,
+  },
+  replicaDesc: {
+    marginBottom: theme.spacing(2),
+    maxWidth: 480,
+  },
+  toggle: {
+    marginBottom: theme.spacing(2),
+  },
+  icon: {
+    fontSize: '20px',
+    marginLeft: theme.spacing(0.5),
+  },
+}));
+
+const LoadCollectionDialog = (props: any) => {
+  const classes = useStyles();
+  const { collection, onLoad } = props;
+  const { t: dialogTrans } = useTranslation('dialog');
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: warningTrans } = useTranslation('warning');
+  const { handleCloseDialog } = useContext(rootContext);
+  const [form, setForm] = useState({
+    replica: 0,
+  });
+  const [enableRelica, setEnableRelica] = useState(false);
+  const [replicaToggle, setReplicaToggle] = useState(false);
+
+  // check if it is cluster
+  useEffect(() => {
+    async function fetchData() {
+      const res = await MilvusHttp.getMetrics();
+      const parsedJson = parseJson(res);
+      // get root cord
+      const rootCoords = getNode(
+        parsedJson.workingNodes,
+        MILVUS_NODE_TYPE.ROOTCOORD
+      );
+      // get query nodes
+      const queryNodes = getNode(
+        parsedJson.workingNodes,
+        MILVUS_NODE_TYPE.QUERYNODE
+      );
+
+      const rootCoord = rootCoords[0];
+
+      // should we show replic toggle
+      const enableRelica =
+        rootCoord.infos.system_info.deploy_mode ===
+        MILVUS_DEPLOY_MODE.DISTRIBUTED;
+
+      // only show replica toggle in distributed mode && query node > 1
+      if (enableRelica && queryNodes.length > 1) {
+        setForm({
+          replica: queryNodes.length,
+        });
+        setEnableRelica(enableRelica);
+      }
+    }
+    fetchData();
+  }, []);
+
+  // input  state change
+  const handleInputChange = (value: number) => {
+    setForm({ replica: value });
+  };
+  // confirm action
+  const handleConfirm = async () => {
+    let params;
+
+    if (enableRelica) {
+      params = { replica_number: Number(form.replica) };
+    }
+
+    // load collection request
+    await CollectionHttp.loadCollection(collection, params);
+    // close dialog
+    handleCloseDialog();
+    // callback
+    onLoad && onLoad();
+  };
+
+  // validator
+  const checkedForm = useMemo(() => {
+    return enableRelica ? [] : formatForm(form);
+  }, [form, enableRelica]);
+
+  // validate
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  // input config
+  const inputConfig: ITextfieldConfig = {
+    label: collectionTrans('replicaNum'),
+    type: 'number',
+    key: 'replica',
+    onChange: handleInputChange,
+    variant: 'filled',
+    placeholder: collectionTrans('replicaNum'),
+    fullWidth: true,
+    validations: [],
+    required: enableRelica,
+    defaultValue: form.replica,
+  };
+
+  // if replica is enabled, add validation
+  if (enableRelica) {
+    inputConfig.validations?.push({
+      rule: 'require',
+      errorText: warningTrans('required', {
+        name: collectionTrans('replicaNum'),
+      }),
+    });
+  }
+
+  // toggle enbale replica
+  const handleChange = () => {
+    setReplicaToggle(!replicaToggle);
+  };
+
+  const InfoIcon = icons.info;
+
+  return (
+    <DialogTemplate
+      title={dialogTrans('loadTitle', {
+        type: collection,
+      })}
+      handleClose={handleCloseDialog}
+      children={
+        <>
+          <Typography variant="body1" component="p" className={classes.desc}>
+            {collectionTrans('loadContent')}
+          </Typography>
+          {enableRelica ? (
+            <>
+              <FormControlLabel
+                control={
+                  <Switch checked={replicaToggle} onChange={handleChange} />
+                }
+                label={
+                  <CustomToolTip title={collectionTrans('replicaDes')}>
+                    <>
+                      {collectionTrans('enableRepica')}
+                      <InfoIcon classes={{ root: classes.icon }} />
+                    </>
+                  </CustomToolTip>
+                }
+                className={classes.toggle}
+              />
+            </>
+          ) : null}
+          {replicaToggle ? (
+            <>
+              <CustomInput
+                type="text"
+                textConfig={inputConfig}
+                checkValid={checkIsValid}
+                validInfo={validation}
+              />
+            </>
+          ) : null}
+        </>
+      }
+      confirmLabel={btnTrans('load')}
+      handleConfirm={handleConfirm}
+      confirmDisabled={replicaToggle ? disabled : false}
+    />
+  );
+};
+
+export default LoadCollectionDialog;

+ 63 - 0
client/src/components/dialogs/ReleaseCollectionDialog.tsx

@@ -0,0 +1,63 @@
+import { useContext, useState } from 'react';
+import { Typography, makeStyles, Theme } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { CollectionHttp } from '../../http/Collection';
+import { rootContext } from '../../context/Root';
+import DialogTemplate from '../customDialog/DialogTemplate';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  desc: {
+    margin: '8px 0 16px 0',
+    maxWidth: 480,
+  },
+}));
+
+const ReleaseCollectionDialog = (props: any) => {
+  const classes = useStyles();
+
+  const { collection, onRelease } = props;
+  const { t: dialogTrans } = useTranslation('dialog');
+  const { t: btnTrans } = useTranslation('btn');
+  const { handleCloseDialog } = useContext(rootContext);
+  const [disabled, setDisabled] = useState(false);
+
+  // confirm action
+  const handleConfirm = async () => {
+    // disable confirm button
+    setDisabled(true);
+    try {
+      // release collection
+      await CollectionHttp.releaseCollection(collection);
+      // execute callback
+      onRelease && onRelease();
+      // enable confirm button
+      setDisabled(false);
+      // close dialog
+      handleCloseDialog();
+    } finally {
+      // enable confirm button
+      setDisabled(false);
+    }
+  };
+
+  return (
+    <DialogTemplate
+      title={dialogTrans('releaseTitle', {
+        type: collection,
+      })}
+      handleClose={handleCloseDialog}
+      children={
+        <>
+          <Typography variant="body1" component="p" className={classes.desc}>
+            {dialogTrans('releaseContent', { type: collection })}
+          </Typography>
+        </>
+      }
+      confirmLabel={btnTrans('release')}
+      handleConfirm={handleConfirm}
+      confirmDisabled={disabled}
+    />
+  );
+};
+
+export default ReleaseCollectionDialog;

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

@@ -216,4 +216,20 @@ export enum LOADING_STATE {
 
 
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;
-export const DEFAULT_MILVUS_PORT = 19530;
+export const DEFAULT_MILVUS_PORT = 19530;
+
+export enum MILVUS_NODE_TYPE {
+  ROOTCOORD = 'rootcoord',
+  QUERYCOORD = 'querycoord',
+  INDEXCOORD = 'indexcoord',
+  QUERYNODE = 'querynode',
+  INDEXNODE = 'indexnode',
+  DATACORD = 'datacord',
+  DATANODE = 'datanode',
+  PROXY = 'proxy',
+}
+
+export enum MILVUS_DEPLOY_MODE {
+  DISTRIBUTED = 'DISTRIBUTED',
+  STANDALONE = 'STANDALONE',
+}

+ 0 - 71
client/src/hooks/Dialog.tsx

@@ -1,76 +1,5 @@
 import { ReactElement, useContext } from 'react';
 import { ReactElement, useContext } from 'react';
-import { useTranslation } from 'react-i18next';
-import { Typography } from '@material-ui/core';
 import { rootContext } from '../context/Root';
 import { rootContext } from '../context/Root';
-import { CollectionData, CollectionView } from '../pages/collections/Types';
-import { PartitionView } from '../pages/partitions/Types';
-import { LOADING_STATE } from '../consts/Milvus';
-
-// handle release and load dialog
-export interface LoadAndReleaseDialogHookProps {
-  type: 'partition' | 'collection';
-}
-
-export const useLoadAndReleaseDialogHook = (
-  props: LoadAndReleaseDialogHookProps
-) => {
-  const { type } = props;
-  const { setDialog } = useContext(rootContext);
-  const { t: dialogTrans } = useTranslation('dialog');
-  const { t: btnTrans } = useTranslation('btn');
-  const { t: partitionTrans } = useTranslation('partition');
-  const { t: collectionTrans } = useTranslation('collection');
-
-  const name =
-    type === 'collection'
-      ? collectionTrans('collection')
-      : partitionTrans('partition');
-
-  const actionsMap = {
-    release: {
-      title: dialogTrans('releaseTitle', { type: name }),
-      component: (
-        <Typography className="dialog-content">
-          {dialogTrans('releaseContent', { type: name })}
-        </Typography>
-      ),
-      confirmLabel: btnTrans('release'),
-    },
-    load: {
-      title: dialogTrans('loadTitle', { type: name }),
-      component: (
-        <Typography className="dialog-content">
-          {dialogTrans('loadContent', { type: name })}
-        </Typography>
-      ),
-      confirmLabel: btnTrans('load'),
-    },
-  };
-
-  const handleAction = (
-    data: PartitionView | CollectionView | CollectionData,
-    cb: (data: any) => Promise<any>
-  ) => {
-    const actionType: 'release' | 'load' =
-      data._status === LOADING_STATE.UNLOADED ? 'load' : 'release';
-    const { title, component, confirmLabel } = actionsMap[actionType];
-
-    setDialog({
-      open: true,
-      type: 'notice',
-      params: {
-        title,
-        component,
-        confirmLabel,
-        confirm: () => cb(data),
-      },
-    });
-  };
-
-  return {
-    handleAction,
-  };
-};
 
 
 export const useInsertDialogHook = () => {
 export const useInsertDialogHook = () => {
   const { setDialog } = useContext(rootContext);
   const { setDialog } = useContext(rootContext);

+ 9 - 1
client/src/http/Collection.ts

@@ -4,6 +4,8 @@ import {
   DeleteEntitiesReq,
   DeleteEntitiesReq,
   InsertDataParam,
   InsertDataParam,
   LoadSampleParam,
   LoadSampleParam,
+  LoadRelicaReq,
+  Replica,
 } from '../pages/collections/Types';
 } from '../pages/collections/Types';
 import { Field } from '../pages/schema/Types';
 import { Field } from '../pages/schema/Types';
 import { VectorSearchParam } from '../types/SearchTypes';
 import { VectorSearchParam } from '../types/SearchTypes';
@@ -29,6 +31,7 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   private schema!: {
   private schema!: {
     fields: Field[];
     fields: Field[];
   };
   };
+  private replicas!: Replica[];
 
 
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
@@ -67,9 +70,10 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     return super.delete({ path: `${this.COLLECTIONS_URL}/${collectionName}` });
     return super.delete({ path: `${this.COLLECTIONS_URL}/${collectionName}` });
   }
   }
 
 
-  static loadCollection(collectionName: string) {
+  static loadCollection(collectionName: string, param?: LoadRelicaReq) {
     return super.update({
     return super.update({
       path: `${this.COLLECTIONS_URL}/${collectionName}/load`,
       path: `${this.COLLECTIONS_URL}/${collectionName}/load`,
+      data: param,
     });
     });
   }
   }
 
 
@@ -195,4 +199,8 @@ export class CollectionHttp extends BaseModel implements CollectionView {
       ? dayjs(Number(this.createdTime)).format('YYYY-MM-DD HH:mm:ss')
       ? dayjs(Number(this.createdTime)).format('YYYY-MM-DD HH:mm:ss')
       : '';
       : '';
   }
   }
+
+  get _replicas(): Replica[] {
+    return this.replicas;
+  }
 }
 }

+ 4 - 1
client/src/i18n/en/collection.ts

@@ -58,8 +58,11 @@ const collectionTrans = {
   // load dialog
   // load dialog
   loadTitle: 'Load Collection',
   loadTitle: 'Load Collection',
   loadContent:
   loadContent:
-    'You are trying to load a collection with data. Only loaded collection can be searched.',
+    'All search and query operations within Milvus are executed in memory, only loaded collection can be searched.',
   loadConfirmLabel: 'Load',
   loadConfirmLabel: 'Load',
+  replicaNum: 'Replica number',
+  replicaDes: `With in-memory replicas, Milvus can load the same segment on multiple query nodes. The replica number can not exceed query node count.`,
+  enableRepica: `Enable in-memory replica`,
 
 
   // release dialog
   // release dialog
   releaseTitle: 'Release Collection',
   releaseTitle: 'Release Collection',

+ 26 - 7
client/src/pages/collections/Collection.tsx

@@ -1,22 +1,40 @@
+import { useMemo } from 'react';
+import { useNavigate, useLocation, useParams } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme } from '@material-ui/core';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import CustomTabList from '../../components/customTabList/CustomTabList';
 import CustomTabList from '../../components/customTabList/CustomTabList';
 import { ITab } from '../../components/customTabList/Types';
 import { ITab } from '../../components/customTabList/Types';
 import Partitions from '../partitions/Partitions';
 import Partitions from '../partitions/Partitions';
-import { useNavigate, useLocation, useParams } from 'react-router-dom';
-import { useMemo } from 'react';
 import { parseLocationSearch } from '../../utils/Format';
 import { parseLocationSearch } from '../../utils/Format';
 import Schema from '../schema/Schema';
 import Schema from '../schema/Schema';
 import Query from '../query/Query';
 import Query from '../query/Query';
 import Preview from '../preview/Preview';
 import Preview from '../preview/Preview';
+import { TAB_EMUM } from './Types';
 
 
-enum TAB_EMUM {
-  'schema',
-  'partition',
-}
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    flexDirection: 'row',
+    gap: theme.spacing(4),
+  },
+  card: {
+    boxShadow: 'none',
+    flexBasis: theme.spacing(28),
+    width: theme.spacing(28),
+    flexGrow: 0,
+    flexShrink: 0,
+  },
+  tab: {
+    flexGrow: 1,
+    flexShrink: 1,
+    overflowX: 'auto',
+  },
+}));
 
 
 const Collection = () => {
 const Collection = () => {
+  const classes = useStyles();
+
   const { collectionName = '' } = useParams<{
   const { collectionName = '' } = useParams<{
     collectionName: string;
     collectionName: string;
   }>();
   }>();
@@ -60,9 +78,10 @@ const Collection = () => {
   ];
   ];
 
 
   return (
   return (
-    <section className="page-wrapper">
+    <section className={`page-wrapper ${classes.wrapper}`}>
       <CustomTabList
       <CustomTabList
         tabs={tabs}
         tabs={tabs}
+        wrapperClass={classes.tab}
         activeIndex={activeTabIndex}
         activeIndex={activeTabIndex}
         handleTabChange={handleTabChange}
         handleTabChange={handleTabChange}
       />
       />

+ 24 - 16
client/src/pages/collections/Collections.tsx

@@ -25,10 +25,9 @@ import { rootContext } from '../../context/Root';
 import CreateCollection from './Create';
 import CreateCollection from './Create';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import { CollectionHttp } from '../../http/Collection';
 import { CollectionHttp } from '../../http/Collection';
-import {
-  useInsertDialogHook,
-  useLoadAndReleaseDialogHook,
-} from '../../hooks/Dialog';
+import { useInsertDialogHook } from '../../hooks/Dialog';
+import LoadCollectionDialog from '../../components/dialogs/LoadCollectionDialog';
+import ReleaseCollectionDialog from '../../components/dialogs/ReleaseCollectionDialog';
 import Highlighter from 'react-highlight-words';
 import Highlighter from 'react-highlight-words';
 import InsertContainer from '../../components/insert/Container';
 import InsertContainer from '../../components/insert/Container';
 import ImportSample from '../../components/insert/ImportSample';
 import ImportSample from '../../components/insert/ImportSample';
@@ -64,7 +63,6 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const Collections = () => {
 const Collections = () => {
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
-  const { handleAction } = useLoadAndReleaseDialogHook({ type: 'collection' });
   const { handleInsertDialog } = useInsertDialogHook();
   const { handleInsertDialog } = useInsertDialogHook();
   const [searchParams] = useSearchParams();
   const [searchParams] = useSearchParams();
   const [search, setSearch] = useState<string>(
   const [search, setSearch] = useState<string>(
@@ -246,20 +244,16 @@ const Collections = () => {
     fetchData();
     fetchData();
   };
   };
 
 
-  const handleRelease = async (data: CollectionView) => {
-    const res = await CollectionHttp.releaseCollection(data._name);
+  const onRelease = async () => {
     openSnackBar(
     openSnackBar(
       successTrans('release', { name: collectionTrans('collection') })
       successTrans('release', { name: collectionTrans('collection') })
     );
     );
     fetchData();
     fetchData();
-    return res;
   };
   };
 
 
-  const handleLoad = async (data: CollectionView) => {
-    const res = await CollectionHttp.loadCollection(data._name);
+  const onLoad = () => {
     openSnackBar(successTrans('load', { name: collectionTrans('collection') }));
     openSnackBar(successTrans('load', { name: collectionTrans('collection') }));
     fetchData();
     fetchData();
-    return res;
   };
   };
 
 
   const handleDelete = async () => {
   const handleDelete = async () => {
@@ -448,11 +442,25 @@ const Collections = () => {
       actionBarConfigs: [
       actionBarConfigs: [
         {
         {
           onClick: (e: React.MouseEvent, row: CollectionView) => {
           onClick: (e: React.MouseEvent, row: CollectionView) => {
-            const cb =
-              row._status === LOADING_STATE.UNLOADED
-                ? handleLoad
-                : handleRelease;
-            handleAction(row, cb);
+            setDialog({
+              open: true,
+              type: 'custom',
+              params: {
+                component:
+                  row._status === LOADING_STATE.UNLOADED ? (
+                    <LoadCollectionDialog
+                      collection={row._name}
+                      onLoad={onLoad}
+                    />
+                  ) : (
+                    <ReleaseCollectionDialog
+                      collection={row._name}
+                      onRelease={onRelease}
+                    />
+                  ),
+              },
+            });
+
             e.preventDefault();
             e.preventDefault();
           },
           },
           icon: 'load',
           icon: 'load',

+ 20 - 0
client/src/pages/collections/Types.ts

@@ -14,6 +14,22 @@ export interface CollectionData {
   _fields?: FieldData[];
   _fields?: FieldData[];
   _consistencyLevel?: string;
   _consistencyLevel?: string;
   _aliases: string[];
   _aliases: string[];
+  _replicas: Replica[];
+}
+
+export interface Replica {
+  collectionID: string;
+  node_ids: string[];
+  partition_ids: string[];
+  replicaID: string;
+  shard_replicas: ShardReplica[];
+}
+
+export interface ShardReplica {
+  dm_channel_name: string;
+  leaderID: string;
+  leader_addr: string;
+  node_id: string[];
 }
 }
 
 
 export interface CollectionView extends CollectionData {
 export interface CollectionView extends CollectionData {
@@ -129,6 +145,10 @@ export interface AliasesProps {
   onDelete?: Function;
   onDelete?: Function;
 }
 }
 
 
+export interface LoadRelicaReq {
+  replica_number: number;
+}
+
 export enum TAB_EMUM {
 export enum TAB_EMUM {
   'schema',
   'schema',
   'partition',
   'partition',

+ 2 - 13
client/src/pages/overview/Overview.tsx

@@ -7,7 +7,6 @@ import { WS_EVENTS, WS_EVENTS_TYPE } from '../../consts/Http';
 import { LOADING_STATE } from '../../consts/Milvus';
 import { LOADING_STATE } from '../../consts/Milvus';
 import { rootContext } from '../../context/Root';
 import { rootContext } from '../../context/Root';
 import { webSokcetContext } from '../../context/WebSocket';
 import { webSokcetContext } from '../../context/WebSocket';
-import { useLoadAndReleaseDialogHook } from '../../hooks/Dialog';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { CollectionHttp } from '../../http/Collection';
 import { CollectionHttp } from '../../http/Collection';
 import { MilvusHttp } from '../../http/Milvus';
 import { MilvusHttp } from '../../http/Milvus';
@@ -34,7 +33,6 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const Overview = () => {
 const Overview = () => {
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
-  const { handleAction } = useLoadAndReleaseDialogHook({ type: 'collection' });
   const classes = useStyles();
   const classes = useStyles();
   const theme = useTheme();
   const theme = useTheme();
   const { t: overviewTrans } = useTranslation('overview');
   const { t: overviewTrans } = useTranslation('overview');
@@ -48,9 +46,7 @@ const Overview = () => {
     totalData: 0,
     totalData: 0,
   });
   });
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
-
   const { collections, setCollections } = useContext(webSokcetContext);
   const { collections, setCollections } = useContext(webSokcetContext);
-
   const { openSnackBar } = useContext(rootContext);
   const { openSnackBar } = useContext(rootContext);
 
 
   const fetchData = useCallback(async () => {
   const fetchData = useCallback(async () => {
@@ -81,18 +77,11 @@ const Overview = () => {
     [collections]
     [collections]
   );
   );
 
 
-  const fetchRelease = async (data: CollectionData) => {
-    const name = data._name;
-    const res = await CollectionHttp.releaseCollection(name);
+  const onRelease = () => {
     openSnackBar(
     openSnackBar(
       successTrans('release', { name: collectionTrans('collection') })
       successTrans('release', { name: collectionTrans('collection') })
     );
     );
     fetchData();
     fetchData();
-    return res;
-  };
-
-  const handleRelease = (data: CollectionData) => {
-    handleAction(data, fetchRelease);
   };
   };
 
 
   const statisticsData = useMemo(() => {
   const statisticsData = useMemo(() => {
@@ -134,7 +123,7 @@ const Overview = () => {
             <CollectionCard
             <CollectionCard
               key={collection._id}
               key={collection._id}
               data={collection}
               data={collection}
-              handleRelease={handleRelease}
+              onRelease={onRelease}
             />
             />
           ))}
           ))}
         </div>
         </div>

+ 37 - 16
client/src/pages/overview/collectionCard/CollectionCard.tsx

@@ -1,5 +1,5 @@
 import { makeStyles, Theme, Typography, Divider } from '@material-ui/core';
 import { makeStyles, Theme, Typography, Divider } from '@material-ui/core';
-import { FC } from 'react';
+import { FC, useContext } from 'react';
 import CustomButton from '../../../components/customButton/CustomButton';
 import CustomButton from '../../../components/customButton/CustomButton';
 import icons from '../../../components/icons/Icons';
 import icons from '../../../components/icons/Icons';
 import Status from '../../../components/status/Status';
 import Status from '../../../components/status/Status';
@@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next';
 import CustomIconButton from '../../../components/customButton/CustomIconButton';
 import CustomIconButton from '../../../components/customButton/CustomIconButton';
 import { useNavigate, Link } from 'react-router-dom';
 import { useNavigate, Link } from 'react-router-dom';
 import { LOADING_STATE } from '../../../consts/Milvus';
 import { LOADING_STATE } from '../../../consts/Milvus';
+import ReleaseCollectionDialog from '../../../components/dialogs/ReleaseCollectionDialog';
+import { rootContext } from '../../../context/Root';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
@@ -36,9 +38,13 @@ const useStyles = makeStyles((theme: Theme) => ({
     fontSize: '16px',
     fontSize: '16px',
   },
   },
   content: {
   content: {
-    display: 'flex',
-    alignItems: 'center',
-    marginBottom: theme.spacing(2),
+    margin: 0,
+    padding: 0,
+    '& > li': {
+      display: 'flex',
+      alignItems: 'center',
+      marginBottom: theme.spacing(0.5),
+    },
   },
   },
   rowCount: {
   rowCount: {
     marginLeft: theme.spacing(1),
     marginLeft: theme.spacing(1),
@@ -70,15 +76,18 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const CollectionCard: FC<CollectionCardProps> = ({
 const CollectionCard: FC<CollectionCardProps> = ({
   data,
   data,
-  handleRelease,
+  onRelease,
   wrapperClass = '',
   wrapperClass = '',
 }) => {
 }) => {
   const classes = useStyles();
   const classes = useStyles();
+  const { setDialog } = useContext(rootContext);
+
   const {
   const {
     _name: name,
     _name: name,
     _status: status,
     _status: status,
     _rowCount: rowCount,
     _rowCount: rowCount,
     _loadedPercentage,
     _loadedPercentage,
+    _replicas,
   } = data;
   } = data;
   const navigate = useNavigate();
   const navigate = useNavigate();
   // icons
   // icons
@@ -91,7 +100,15 @@ const CollectionCard: FC<CollectionCardProps> = ({
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
 
 
   const onReleaseClick = () => {
   const onReleaseClick = () => {
-    handleRelease(data);
+    setDialog({
+      open: true,
+      type: 'custom',
+      params: {
+        component: (
+          <ReleaseCollectionDialog collection={name} onRelease={onRelease} />
+        ),
+      },
+    });
   };
   };
 
 
   const onVectorSearchClick = () => {
   const onVectorSearchClick = () => {
@@ -111,16 +128,20 @@ const CollectionCard: FC<CollectionCardProps> = ({
         {name}
         {name}
         <RightArrowIcon classes={{ root: classes.icon }} />
         <RightArrowIcon classes={{ root: classes.icon }} />
       </Link>
       </Link>
-      <div className={classes.content}>
-        <Typography>{collectionTrans('rowCount')}</Typography>
-        <CustomToolTip
-          title={collectionTrans('entityCountInfo')}
-          placement="bottom"
-        >
-          <InfoIcon classes={{ root: classes.icon }} />
-        </CustomToolTip>
-        <Typography className={classes.rowCount}>{rowCount}</Typography>
-      </div>
+      <ul className={classes.content}>
+        {_replicas.length > 1 ? (
+          <li>
+            <Typography>{collectionTrans('replicaNum')}</Typography>:
+            <Typography className={classes.rowCount}>
+              {_replicas.length}
+            </Typography>
+          </li>
+        ) : null}
+        <li>
+          <Typography>{collectionTrans('rowCount')}</Typography>:
+          <Typography className={classes.rowCount}>{rowCount}</Typography>
+        </li>
+      </ul>
       <Divider classes={{ root: classes.divider }} />
       <Divider classes={{ root: classes.divider }} />
       <CustomButton
       <CustomButton
         variant="contained"
         variant="contained"

+ 1 - 1
client/src/pages/overview/collectionCard/Types.ts

@@ -2,6 +2,6 @@ import { CollectionData } from '../../collections/Types';
 
 
 export interface CollectionCardProps {
 export interface CollectionCardProps {
   data: CollectionData;
   data: CollectionData;
-  handleRelease: (data: CollectionData) => void;
+  onRelease: () => void;
   wrapperClass?: string;
   wrapperClass?: string;
 }
 }

+ 1 - 1
client/src/pages/partitions/Partitions.tsx

@@ -24,7 +24,7 @@ import { MilvusHttp } from '../../http/Milvus';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
-    height: '100%',
+    height: `calc(100vh - 160px)`,
   },
   },
   icon: {
   icon: {
     fontSize: '20px',
     fontSize: '20px',

+ 1 - 1
client/src/pages/schema/Schema.tsx

@@ -13,7 +13,7 @@ import { IndexHttp } from '../../http/Index';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
-    height: '100%',
+    height: `calc(100vh - 160px)`,
   },
   },
   icon: {
   icon: {
     fontSize: '20px',
     fontSize: '20px',

+ 1 - 43
client/src/pages/system/SystemView.tsx

@@ -11,6 +11,7 @@ import NodeListView from './NodeListView';
 // import LineChartCard from './LineChartCard';
 // import LineChartCard from './LineChartCard';
 // import ProgressCard from './ProgressCard';
 // import ProgressCard from './ProgressCard';
 import DataCard from './DataCard';
 import DataCard from './DataCard';
+import { parseJson } from '../../utils/Metric';
 
 
 const getStyles = makeStyles((theme: Theme) => ({
 const getStyles = makeStyles((theme: Theme) => ({
   root: {
   root: {
@@ -62,49 +63,6 @@ const getStyles = makeStyles((theme: Theme) => ({
   },
   },
 }));
 }));
 
 
-const parseJson = (jsonData: any) => {
-  const nodes: any[] = [];
-  const childNodes: any[] = [];
-
-  const system = {
-    // qps: Math.random() * 1000,
-    latency: Math.random() * 1000,
-    disk: 0,
-    diskUsage: 0,
-    memory: 0,
-    memoryUsage: 0,
-  };
-
-  const workingNodes = jsonData?.response?.nodes_info.filter(
-    (node: any) => node?.infos?.has_error !== true
-  );
-
-  workingNodes.forEach((node: any) => {
-    const type = node?.infos?.type;
-    if (node.connected) {
-      node.connected = node.connected.filter((v: any) =>
-        workingNodes.find(
-          (item: any) => v.connected_identifier === item.identifier
-        )
-      );
-    }
-    // coordinator node
-    if (type?.toLowerCase().includes('coord')) {
-      nodes.push(node);
-      // other nodes
-    } else {
-      childNodes.push(node);
-    }
-
-    const info = node.infos.hardware_infos;
-    system.memory += info.memory;
-    system.memoryUsage += info.memory_usage;
-    system.disk += info.disk;
-    system.diskUsage += info.disk_usage;
-  });
-  return { nodes, childNodes, system };
-};
-
 /**
 /**
  * Todo: Milvus V2.0.0 Memory data is not ready for now, open it after Milvus ready.
  * Todo: Milvus V2.0.0 Memory data is not ready for now, open it after Milvus ready.
  * @returns
  * @returns

+ 0 - 5
client/src/styles/common.css

@@ -55,9 +55,4 @@ fieldset {
 .dialog-content {
 .dialog-content {
   line-height: 24px;
   line-height: 24px;
   font-size: 16px;
   font-size: 16px;
-  text-transform: lowercase;
-}
-
-.dialog-content::first-letter {
-  text-transform: uppercase;
 }
 }

+ 1 - 1
client/src/styles/theme.ts

@@ -126,7 +126,7 @@ export const theme = createMuiTheme({
       // style for element p
       // style for element p
       body1: {
       body1: {
         fontSize: '14px',
         fontSize: '14px',
-        lineHeight: '20px',
+        lineHeight: 1.5,
       },
       },
       // small caption
       // small caption
       body2: {
       body2: {

+ 48 - 0
client/src/utils/Metric.ts

@@ -0,0 +1,48 @@
+import { MILVUS_NODE_TYPE } from '../consts/Milvus';
+
+export const parseJson = (jsonData: any) => {
+  const nodes: any[] = [];
+  const childNodes: any[] = [];
+
+  const system = {
+    // qps: Math.random() * 1000,
+    latency: Math.random() * 1000,
+    disk: 0,
+    diskUsage: 0,
+    memory: 0,
+    memoryUsage: 0,
+  };
+
+  const workingNodes = jsonData?.response?.nodes_info.filter(
+    (node: any) => node?.infos?.has_error !== true
+  );
+
+  workingNodes.forEach((node: any) => {
+    const type = node?.infos?.type;
+    if (node.connected) {
+      node.connected = node.connected.filter((v: any) =>
+        workingNodes.find(
+          (item: any) => v.connected_identifier === item.identifier
+        )
+      );
+    }
+    // coordinator node
+    if (type?.toLowerCase().includes('coord')) {
+      nodes.push(node);
+      // other nodes
+    } else {
+      childNodes.push(node);
+    }
+
+    const info = node.infos.hardware_infos;
+    system.memory += info.memory;
+    system.memoryUsage += info.memory_usage;
+    system.disk += info.disk;
+    system.diskUsage += info.disk_usage;
+  });
+  return { nodes, childNodes, system, workingNodes };
+};
+
+export const getNode = (nodes: any, type: MILVUS_NODE_TYPE) => {
+  return nodes.filter((n: any) => n.infos.type === type);
+};

+ 20 - 4
server/src/collections/collections.controller.ts

@@ -10,6 +10,7 @@ import {
   VectorSearchDto,
   VectorSearchDto,
   QueryDto,
   QueryDto,
 } from './dto';
 } from './dto';
+import { LoadCollectionReq } from '@zilliz/milvus2-sdk-node/dist/milvus/types';
 
 
 export class CollectionController {
 export class CollectionController {
   private collectionsService: CollectionsService;
   private collectionsService: CollectionsService;
@@ -179,11 +180,14 @@ export class CollectionController {
   }
   }
 
 
   async loadCollection(req: Request, res: Response, next: NextFunction) {
   async loadCollection(req: Request, res: Response, next: NextFunction) {
-    const name = req.params?.name;
+    const collection_name = req.params?.name;
+    const data = req.body;
+    const param: LoadCollectionReq = { collection_name };
+    if (data.replica_number) {
+      param.replica_number = Number(data.replica_number);
+    }
     try {
     try {
-      const result = await this.collectionsService.loadCollection({
-        collection_name: name,
-      });
+      const result = await this.collectionsService.loadCollection(param);
       res.send(result);
       res.send(result);
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);
@@ -305,4 +309,16 @@ export class CollectionController {
       next(error);
       next(error);
     }
     }
   }
   }
+
+  async getReplicas(req: Request, res: Response, next: NextFunction) {
+    const collectionID = req.params?.collectionID;
+    try {
+      const result = await this.collectionsService.getReplicas({
+        collectionID,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
 }
 }

+ 15 - 3
server/src/collections/collections.service.ts

@@ -20,7 +20,7 @@ import {
   ShowCollectionsReq,
   ShowCollectionsReq,
   ShowCollectionsType,
   ShowCollectionsType,
 } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Collection';
 } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Collection';
-import { QueryDto, ImportSampleDto } from './dto';
+import { QueryDto, ImportSampleDto, GetReplicasDto } from './dto';
 import { DeleteEntitiesReq } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Data';
 import { DeleteEntitiesReq } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Data';
 
 
 export class CollectionsService {
 export class CollectionsService {
@@ -116,6 +116,11 @@ export class CollectionsService {
     return res;
     return res;
   }
   }
 
 
+  async getReplicas(data: GetReplicasDto) {
+    const res = await this.collectionManager.getReplicas(data);
+    return res;
+  }
+
   async query(
   async query(
     data: {
     data: {
       collection_name: string;
       collection_name: string;
@@ -143,7 +148,7 @@ export class CollectionsService {
    * @returns {id:string, collection_name:string, schema:Field[], autoID:boolean, rowCount: string, consistency_level:string}
    * @returns {id:string, collection_name:string, schema:Field[], autoID:boolean, rowCount: string, consistency_level:string}
    */
    */
   async getAllCollections() {
   async getAllCollections() {
-    const data = [];
+    const data: any = [];
     const res = await this.getCollections();
     const res = await this.getCollections();
     const loadedCollections = await this.getCollections({
     const loadedCollections = await this.getCollections({
       type: ShowCollectionsType.Loaded,
       type: ShowCollectionsType.Loaded,
@@ -175,6 +180,12 @@ export class CollectionsService {
           ? '-1'
           ? '-1'
           : loadCollection.loadedPercentage;
           : loadCollection.loadedPercentage;
 
 
+        const replicas: any = loadCollection
+          ? await this.getReplicas({
+              collectionID: collectionInfo.collectionID,
+            })
+          : [];
+
         data.push({
         data.push({
           aliases: collectionInfo.aliases,
           aliases: collectionInfo.aliases,
           collection_name: name,
           collection_name: name,
@@ -187,11 +198,12 @@ export class CollectionsService {
           createdTime: parseInt(collectionInfo.created_utc_timestamp, 10),
           createdTime: parseInt(collectionInfo.created_utc_timestamp, 10),
           index_status: indexRes.state,
           index_status: indexRes.state,
           consistency_level: collectionInfo.consistency_level,
           consistency_level: collectionInfo.consistency_level,
+          replicas: replicas && replicas.replicas,
         });
         });
       }
       }
     }
     }
     // add default sort - Descending order
     // add default sort - Descending order
-    data.sort((a, b) => b.createdTime - a.createdTime);
+    data.sort((a: any, b: any) => b.createdTime - a.createdTime);
     return data;
     return data;
   }
   }
 
 

+ 5 - 0
server/src/collections/dto.ts

@@ -53,6 +53,11 @@ export class ImportSampleDto {
   readonly size: string;
   readonly size: string;
 }
 }
 
 
+export class GetReplicasDto {
+  readonly collectionID: string;
+  readonly with_shard_nodes?: boolean;
+}
+
 export class VectorSearchDto {
 export class VectorSearchDto {
   @IsOptional()
   @IsOptional()
   partition_names?: string[];
   partition_names?: string[];

+ 1 - 1
server/src/crons/crons.controller.ts

@@ -32,7 +32,7 @@ export class CronsController {
   async toggleCronJobByName(req: Request, res: Response, next: NextFunction) {
   async toggleCronJobByName(req: Request, res: Response, next: NextFunction) {
     const cronData = req.body;
     const cronData = req.body;
     const milvusAddress = (req.headers[MILVUS_ADDRESS] as string) || '';
     const milvusAddress = (req.headers[MILVUS_ADDRESS] as string) || '';
-    console.log(cronData, milvusAddress);
+    // console.log(cronData, milvusAddress);
     try {
     try {
       const result = await this.cronsService.toggleCronJobByName({
       const result = await this.cronsService.toggleCronJobByName({
         ...cronData,
         ...cronData,