浏览代码

Merge branch 'main' of github.com:milvus-io/milvus-insight into main

nameczz 4 年之前
父节点
当前提交
9339aa123c
共有 36 个文件被更改,包括 1774 次插入316 次删除
  1. 2 0
      README.md
  2. 1 0
      client/package.json
  3. 4 0
      client/src/assets/icons/copy.svg
  4. 191 0
      client/src/components/advancedSearch/Condition.tsx
  5. 234 0
      client/src/components/advancedSearch/ConditionGroup.tsx
  6. 56 0
      client/src/components/advancedSearch/CopyButton.tsx
  7. 219 0
      client/src/components/advancedSearch/Dialog.tsx
  8. 309 0
      client/src/components/advancedSearch/Filter.tsx
  9. 82 0
      client/src/components/advancedSearch/Types.ts
  10. 3 0
      client/src/components/advancedSearch/index.tsx
  11. 18 13
      client/src/components/customDialog/DialogTemplate.tsx
  12. 1 0
      client/src/components/customDialog/Types.ts
  13. 4 2
      client/src/components/grid/ToolBar.tsx
  14. 2 0
      client/src/components/grid/Types.ts
  15. 4 0
      client/src/components/icons/Icons.tsx
  16. 2 1
      client/src/components/icons/Types.ts
  17. 41 26
      client/src/consts/Milvus.tsx
  18. 3 1
      client/src/context/Auth.tsx
  19. 4 1
      client/src/hooks/Form.ts
  20. 1 0
      client/src/i18n/cn/collection.ts
  21. 1 0
      client/src/i18n/en/collection.ts
  22. 2 0
      client/src/pages/collections/Collections.tsx
  23. 29 1
      client/src/pages/connect/Connect.tsx
  24. 9 5
      client/src/pages/schema/Create.tsx
  25. 1 0
      client/src/pages/schema/Schema.tsx
  26. 10 5
      client/src/pages/schema/Types.ts
  27. 17 0
      client/src/utils/Common.ts
  28. 7 7
      client/src/utils/Form.ts
  29. 88 2
      client/yarn.lock
  30. 5 0
      server/package.json
  31. 5 0
      server/src/cache/config.ts
  32. 32 4
      server/src/collections/collections.controller.ts
  33. 9 3
      server/src/collections/collections.module.ts
  34. 42 0
      server/src/events/events.gateway.ts
  35. 7 0
      server/src/events/events.module.ts
  36. 329 245
      server/yarn.lock

+ 2 - 0
README.md

@@ -38,6 +38,8 @@ docker run -p 8000:3000 -e HOST_URL=http://127.0.0.1:8000 -e MILVUS_URL=127.0.0.
 
 Once you start the docker, open the browser, type `http://127.0.0.1:8000`, you can view the milvus insight.
 
+***note*** We plan to release milvus insight once a feature is done. Also, if you want to try the nightly build, please pull the docker image with the `dev` tag.
+
 ## ✨ Building and Running Milvus insight, and/or Contributing Code
 
 You might want to build Milvus-insight locally to contribute some code, test out the latest features, or try

+ 1 - 0
client/package.json

@@ -30,6 +30,7 @@
     "react-i18next": "^11.10.0",
     "react-router-dom": "^5.2.0",
     "react-scripts": "4.0.3",
+    "socket.io-client": "^4.1.3",
     "typescript": "^4.1.2",
     "web-vitals": "^1.0.1"
   },

+ 4 - 0
client/src/assets/icons/copy.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.3333 6H7.33333C6.59695 6 6 6.59695 6 7.33333V13.3333C6 14.0697 6.59695 14.6667 7.33333 14.6667H13.3333C14.0697 14.6667 14.6667 14.0697 14.6667 13.3333V7.33333C14.6667 6.59695 14.0697 6 13.3333 6Z" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.33301 9.99992H2.66634C2.31272 9.99992 1.97358 9.85944 1.72353 9.60939C1.47348 9.35935 1.33301 9.02021 1.33301 8.66659V2.66659C1.33301 2.31296 1.47348 1.97382 1.72353 1.72378C1.97358 1.47373 2.31272 1.33325 2.66634 1.33325H8.66634C9.01996 1.33325 9.3591 1.47373 9.60915 1.72378C9.8592 1.97382 9.99967 2.31296 9.99967 2.66659V3.33325" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 191 - 0
client/src/components/advancedSearch/Condition.tsx

@@ -0,0 +1,191 @@
+import React, { useState, useEffect, FC } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  IconButton,
+  TextField,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+import { ConditionProps, Field } from './Types';
+import CustomSelector from '../customSelector/CustomSelector';
+
+// Todo: Move to corrsponding Constant file.
+// Static logical operators.
+const LogicalOperators = [
+  {
+    value: '<',
+    label: '<',
+  },
+  {
+    value: '<=',
+    label: '<=',
+  },
+  {
+    value: '>',
+    label: '>',
+  },
+  {
+    value: '>=',
+    label: '>=',
+  },
+  {
+    value: '==',
+    label: '==',
+  },
+  {
+    value: '!=',
+    label: '!=',
+  },
+  {
+    value: 'in',
+    label: 'in',
+  },
+];
+
+const Condition: FC<ConditionProps> = props => {
+  const {
+    onDelete,
+    triggerChange,
+    fields = [],
+    id = '',
+    initData,
+    className = '',
+    ...others
+  } = props;
+  const [operator, setOperator] = useState(
+    initData?.op || LogicalOperators[0].value
+  );
+  const [conditionField, setConditionField] = useState<Field | any>(
+    initData?.field || fields[0] || {}
+  );
+  const [conditionValue, setConditionValue] = useState(initData?.value || '');
+  const [isValuelegal, setIsValueLegal] = useState(
+    initData?.isCorrect || false
+  );
+
+  /**
+   * Check condition's value by field's and operator's type.
+   * Trigger condition change event.
+   */
+  useEffect(() => {
+    const regInt = /^\d+$/;
+    const regFloat = /^\d+\.\d+$/;
+    const regIntInterval = /^\[\d+,\d+\]$/;
+    const regFloatInterval = /^\[\d+\.\d+,\d+\.\d+]$/;
+
+    const type = conditionField?.type;
+    const isIn = operator === 'in';
+    let isLegal = false;
+
+    switch (type) {
+      case 'int':
+        isLegal = isIn
+          ? regIntInterval.test(conditionValue)
+          : regInt.test(conditionValue);
+        break;
+      case 'float':
+        isLegal = isIn
+          ? regFloatInterval.test(conditionValue)
+          : regFloat.test(conditionValue);
+        break;
+      default:
+        isLegal = false;
+        break;
+    }
+    setIsValueLegal(isLegal);
+    triggerChange(id, {
+      field: conditionField,
+      op: operator,
+      value: conditionValue,
+      isCorrect: isLegal,
+      id,
+    });
+    // No need for 'id' and 'triggerChange'.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [conditionField, operator, conditionValue]);
+
+  const classes = useStyles();
+
+  // Logic operator input change.
+  const handleOpChange = (event: React.ChangeEvent<{ value: unknown }>) => {
+    setOperator(event.target.value);
+  };
+  // Field Name input change.
+  const handleFieldNameChange = (
+    event: React.ChangeEvent<{ value: unknown }>
+  ) => {
+    const value = event.target.value;
+    const target = fields.find(field => field.name === value);
+    target && setConditionField(target);
+  };
+  // Value input change.
+  const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const value = event.target.value;
+    setConditionValue(value);
+  };
+
+  return (
+    <div className={`${classes.wrapper} ${className}`} {...others}>
+      <CustomSelector
+        label="Field Name"
+        value={conditionField?.name}
+        onChange={handleFieldNameChange}
+        options={fields.map(i => ({ value: i.name, label: i.name }))}
+        variant="filled"
+        wrapperClass={classes.fieldName}
+      />
+      <CustomSelector
+        label="Logic"
+        value={operator}
+        onChange={handleOpChange}
+        options={LogicalOperators}
+        variant="filled"
+        wrapperClass={classes.logic}
+      />
+      <TextField
+        className={classes.value}
+        label="Value"
+        variant="filled"
+        // size="small"
+        onChange={handleValueChange}
+        value={conditionValue}
+        error={!isValuelegal}
+      />
+      <IconButton
+        aria-label="close"
+        className={classes.closeButton}
+        onClick={onDelete}
+        size="small"
+      >
+        <CloseIcon />
+      </IconButton>
+    </div>
+  );
+};
+
+Condition.displayName = 'Condition';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      minWidth: '466px',
+      minHeight: '62px',
+      background: '#FFFFFF',
+      padding: '12px 16px',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    },
+    closeButton: {},
+    fieldName: {
+      minHeight: '38px',
+      minWidth: '130px',
+    },
+    logic: { minHeight: '38px', minWidth: '70px', margin: '0 24px' },
+    value: { minHeight: '38px', minWidth: '130px' },
+  })
+);
+
+export default Condition;

+ 234 - 0
client/src/components/advancedSearch/ConditionGroup.tsx

@@ -0,0 +1,234 @@
+import React, { useState, FC } from 'react';
+import { makeStyles, Theme, createStyles, Button } from '@material-ui/core';
+import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
+import ConditionItem from './Condition';
+import AddIcon from '@material-ui/icons/Add';
+import {
+  ConditionGroupProps,
+  BinaryLogicalOpProps,
+  AddConditionProps,
+} from './Types';
+
+// "And & or" operator component.
+const BinaryLogicalOp: FC<BinaryLogicalOpProps> = props => {
+  const { onChange, className, initValue = 'and' } = props;
+  const [operator, setOperator] = useState(initValue);
+  const handleChange = (
+    event: React.MouseEvent<HTMLElement>,
+    newOp: string
+  ) => {
+    if (newOp !== null) {
+      setOperator(newOp);
+      onChange(newOp);
+    }
+  };
+  return (
+    <>
+      <div className={`${className} op-${operator}`}>
+        <ToggleButtonGroup
+          value={operator}
+          exclusive
+          onChange={handleChange}
+          aria-label="Binary Logical Operator"
+        >
+          <ToggleButton value="and" aria-label="And">
+            AND
+          </ToggleButton>
+          <ToggleButton value="or" aria-label="Or">
+            OR
+          </ToggleButton>
+        </ToggleButtonGroup>
+        <div className="op-split" />
+      </div>
+    </>
+  );
+};
+
+// "+ Add condition" component.
+const AddCondition: FC<AddConditionProps> = props => {
+  const { className, onClick } = props;
+  return (
+    <Button onClick={onClick} color="primary" className={className}>
+      <AddIcon />
+      Add Condition
+    </Button>
+  );
+};
+
+// Condition group component which contains of BinaryLogicalOp, AddCondition and ConditionItem.
+const ConditionGroup = (props: ConditionGroupProps) => {
+  const {
+    fields = [],
+    handleConditions = {},
+    conditions: flatConditions = [],
+  } = props;
+  const {
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    updateConditionData,
+  } = handleConditions;
+
+  const classes = useStyles();
+
+  const generateClassName = (conditions: any, currentIndex: number) => {
+    let className = '';
+    if (currentIndex === 0 || conditions[currentIndex - 1].type === 'break') {
+      className += 'radius-top';
+    }
+    if (
+      currentIndex === conditions.length - 1 ||
+      conditions[currentIndex + 1].type === 'break'
+    ) {
+      className ? (className = 'radius-all') : (className = 'radius-bottom');
+    }
+    return className;
+  };
+
+  // Generate condition items with operators and add condition btn.
+  const generateConditionItems = (conditions: any[]) => {
+    const conditionsLength = conditions.length;
+    const results = conditions.reduce((prev: any, condition, currentIndex) => {
+      if (currentIndex === conditionsLength - 1) {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={updateConditionData}
+            initData={condition?.data}
+            className={generateClassName(conditions, currentIndex)}
+          />
+        );
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id);
+            }}
+          />
+        );
+      } else if (condition.type === 'break') {
+        prev.pop();
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id, true);
+            }}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+            initValue="or"
+          />
+        );
+      } else if (condition.type === 'condition') {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={updateConditionData}
+            initData={condition?.data}
+            className={generateClassName(conditions, currentIndex)}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+          />
+        );
+      }
+      return prev;
+    }, []);
+    return results;
+  };
+
+  return (
+    <div className={classes.wrapper}>
+      {generateConditionItems(flatConditions)}
+      {flatConditions?.length === 0 && (
+        <AddCondition
+          className={classes.addBtn}
+          onClick={() => {
+            addCondition();
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+ConditionGroup.displayName = 'ConditionGroup';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+
+      '& .op-or': {
+        backgroundColor: 'unset',
+        margin: '16px 0',
+      },
+
+      '& .radius-top': {
+        borderRadius: '8px 8px 0 0',
+      },
+      '& .radius-bottom': {
+        borderRadius: '0 0 8px 8px',
+      },
+      '& .radius-all': {
+        borderRadius: '8px',
+      },
+    },
+    addBtn: {},
+    binaryLogicOp: {
+      width: '100%',
+      backgroundColor: '#FFFFFF',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+      '& .op-split': {
+        height: '1px',
+        backgroundColor: '#E9E9ED',
+        width: '100%',
+      },
+      '& button': {
+        width: '42px',
+        height: '32px',
+        color: '#010E29',
+      },
+      '& button.Mui-selected': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+      '& button.Mui-selected:hover': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+    },
+  })
+);
+
+export default ConditionGroup;

+ 56 - 0
client/src/components/advancedSearch/CopyButton.tsx

@@ -0,0 +1,56 @@
+import React, { useState, FC } from 'react';
+import { makeStyles, Theme, createStyles } from '@material-ui/core';
+import { CopyButtonProps } from './Types';
+import icons from '../icons/Icons';
+import CustomIconButton from '../customButton/CustomIconButton';
+
+const CopyIcon = icons.copyExpression;
+
+const CopyButton: FC<CopyButtonProps> = props => {
+  const {
+    label = 'copy button',
+    icon,
+    className,
+    value = '',
+    ...others
+  } = props;
+  const classes = useStyles();
+  const [tooltipTitle, setTooltipTitle] = useState('Copy');
+
+  const handleClick = (v: string) => {
+    setTooltipTitle('Copied!');
+    navigator.clipboard.writeText(v);
+    setTimeout(() => {
+      setTooltipTitle('Copy');
+    }, 1000);
+  };
+
+  return (
+    <CustomIconButton
+      tooltip={tooltipTitle}
+      aria-label={label}
+      className={`${classes.button} ${className}`}
+      onClick={() => handleClick(value || '')}
+      {...others}
+    >
+      {icon || <CopyIcon style={{ color: 'transparent' }} />}
+    </CustomIconButton>
+  );
+};
+
+CopyButton.displayName = 'CopyButton';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    button: {
+      '& svg': {
+        width: '16px',
+        height: '16px',
+      },
+    },
+    tooltip: {},
+  })
+);
+
+export default CopyButton;

+ 219 - 0
client/src/components/advancedSearch/Dialog.tsx

@@ -0,0 +1,219 @@
+import React, { useEffect } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Typography,
+  Button,
+  IconButton,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+import CachedIcon from '@material-ui/icons/Cached';
+import ConditionGroup from './ConditionGroup';
+import CopyBtn from './CopyButton';
+// import DialogTemplate from '../customDialog/DialogTemplate';
+import { DialogProps } from './Types';
+
+const AdvancedDialog = (props: DialogProps) => {
+  const {
+    open = false,
+    onClose,
+    onSubmit,
+    onReset,
+    onCancel,
+    handleConditions = {},
+    conditions: flatConditions = [],
+    isLegal = false,
+    expression: filterExpression = '',
+    title = 'Advanced Filter',
+    fields = [],
+    ...others
+  } = props;
+  const { addCondition } = handleConditions;
+  const classes = useStyles();
+
+  useEffect(() => {
+    flatConditions.length === 0 && addCondition();
+    // Only need add one condition after dialog's first mount.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  return (
+    <>
+      <Dialog
+        onClose={onClose}
+        aria-labelledby="customized-dialog-title"
+        open={open}
+        maxWidth="xl"
+        className={classes.wrapper}
+        {...others}
+      >
+        <DialogTitle className={classes.dialogTitle} disableTypography>
+          <Typography variant="h5" component="h2">
+            {title}
+          </Typography>
+          <IconButton
+            aria-label="close"
+            className={classes.closeButton}
+            onClick={onCancel}
+            size="small"
+          >
+            <CloseIcon />
+          </IconButton>
+        </DialogTitle>
+        <DialogContent>
+          <div
+            className={`${classes.expResult} ${
+              !isLegal && 'disable-exp'
+            } testcopy`}
+          >
+            {`${isLegal ? filterExpression : 'Filter Expression'}`}
+            {isLegal && (
+              <CopyBtn label="copy expression" value={filterExpression} />
+            )}
+          </div>
+          <div className={classes.expWrapper}>
+            <ConditionGroup
+              fields={fields}
+              handleConditions={handleConditions}
+              conditions={flatConditions}
+            />
+          </div>
+        </DialogContent>
+        <DialogActions className={classes.dialogActions}>
+          <Button
+            onClick={onReset}
+            color="primary"
+            className={classes.resetBtn}
+            size="small"
+          >
+            <CachedIcon />
+            Reset
+          </Button>
+          <div>
+            <Button
+              autoFocus
+              onClick={onCancel}
+              color="primary"
+              className={classes.cancelBtn}
+            >
+              Cancel
+            </Button>
+            <Button
+              onClick={onSubmit}
+              variant="contained"
+              color="primary"
+              className={classes.applyBtn}
+              disabled={!isLegal}
+            >
+              Apply Filters
+            </Button>
+          </div>
+        </DialogActions>
+      </Dialog>
+      {/* <DialogTemplate
+        title={title}
+        handleClose={onClose}
+        showCloseIcon
+        handleConfirm={onSubmit}
+        confirmLabel="Apply Filters"
+        confirmDisabled={!isLegal}
+        handleCancel={onCancel}
+        cancelLabel="Cancel"
+        leftActions={
+          <Button
+            onClick={onReset}
+            color="primary"
+            className={classes.resetBtn}
+            size="small"
+          >
+            <CachedIcon />
+            Reset
+          </Button>
+        }
+      >
+        <div
+          className={`${classes.expResult} ${
+            !isLegal && 'disable-exp'
+          } testcopy`}
+        >
+          {`${isLegal ? filterExpression : 'Filter Expression'}`}
+          {isLegal && (
+            <CopyBtn label="copy expression" value={filterExpression} />
+          )}
+        </div>
+        <div className={classes.expWrapper}>
+          <ConditionGroup
+            fields={fields}
+            handleConditions={handleConditions}
+            conditions={flatConditions}
+          />
+        </div>
+      </DialogTemplate> */}
+    </>
+  );
+};
+
+AdvancedDialog.displayName = 'AdvancedDialog';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      '& .disable-exp': {
+        userSelect: 'none',
+        color: '#AEAEBB',
+      },
+    },
+    closeButton: {
+      color: 'black',
+    },
+    dialogTitle: {
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+    },
+    dialogActions: {
+      justifyContent: 'space-between',
+    },
+    resetBtn: {},
+    cancelBtn: {
+      marginRight: '32px',
+    },
+    applyBtn: {
+      backgroundColor: '#06AFF2',
+      color: 'white',
+    },
+    copyButton: {
+      borderRadius: '0',
+    },
+    expResult: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      minHeight: '40px',
+      marginBottom: '16px',
+      padding: '0 16px',
+      fontFamily: 'Source Code Pro',
+      fontStyle: 'normal',
+      fontWeight: 'normal',
+      fontSize: '16px',
+      lineHeight: '24px',
+    },
+    expWrapper: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      minWidth: '480px',
+      minHeight: '104px',
+      padding: '12px',
+    },
+  })
+);
+
+export default AdvancedDialog;

+ 309 - 0
client/src/components/advancedSearch/Filter.tsx

@@ -0,0 +1,309 @@
+import React, { useState, useEffect } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Button,
+  Chip,
+  Tooltip,
+} from '@material-ui/core';
+import FilterListIcon from '@material-ui/icons/FilterList';
+import AdvancedDialog from './Dialog';
+import { FilterProps, ConditionData } from './Types';
+import { generateIdByHash } from '../../utils/Common';
+
+const Filter = function Filter(props: FilterProps) {
+  const {
+    title = 'title',
+    showTitle = true,
+    className = '',
+    tooltipPlacement = 'top',
+    onSubmit,
+    fields = [],
+    ...others
+  } = props;
+  const classes = useStyles();
+
+  const [open, setOpen] = useState(false);
+  const [conditionSum, setConditionSum] = useState(0);
+  const [flatConditions, setFlatConditions] = useState<any[]>([]);
+  const [initConditions, setInitConditions] = useState<any[]>([]);
+  const [isConditionsLegal, setIsConditionsLegal] = useState(false);
+  const [filterExpression, setFilterExpression] = useState('');
+
+  // Check all conditions are all correct.
+  useEffect(() => {
+    // Calc the sum of conditions.
+    setConditionSum(flatConditions.filter(i => i.type === 'condition').length);
+    for (let i = 0; i < flatConditions.length; i++) {
+      const { data, type } = flatConditions[i];
+      if (type !== 'condition') continue;
+      if (!data) {
+        setIsConditionsLegal(false);
+        return;
+      }
+      if (!data.isCorrect) {
+        setIsConditionsLegal(data.isCorrect);
+        return;
+      }
+    }
+    setIsConditionsLegal(true);
+    generateExpression(flatConditions, setFilterExpression);
+  }, [flatConditions]);
+
+  const setFilteredFlatConditions = (conditions: any[]) => {
+    const newConditions = conditions.reduce((prev, item, currentIndex) => {
+      if (prev.length === 0 && item.type !== 'condition') return prev;
+      if (
+        prev.length &&
+        item.type !== 'condition' &&
+        prev[prev.length - 1].type !== 'condition'
+      )
+        return prev;
+      return [...prev, item];
+    }, []);
+    setFlatConditions(newConditions);
+  };
+
+  const generateExpression = (conditions: any[], func: any) => {
+    const expression = conditions.reduce((prev, item) => {
+      const { type, data } = item;
+      if (type === 'break') return `${prev} || `;
+      const {
+        field: { name },
+        op,
+        value,
+      } = data;
+      return `${prev}${
+        prev && !prev.endsWith('|| ') ? ' && ' : ''
+      }${name} ${op} ${value}`;
+    }, '');
+    func(expression);
+  };
+
+  /**
+   * Insert "OR" operator into specified position.
+   * @param targetId The break operator will be inserted after the target one.
+   */
+  const addOrOp = (targetId?: string) => {
+    if (!targetId) {
+      setFilteredFlatConditions([
+        ...flatConditions,
+        { id: generateIdByHash('break'), type: 'break' },
+        {
+          id: generateIdByHash('condition'),
+          type: 'condition',
+          field: '',
+          operator: '<',
+          value: '',
+        },
+      ]);
+      return;
+    }
+    const formerConditons = [...flatConditions];
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        return [
+          ...prev,
+          item,
+          { id: generateIdByHash('break'), type: 'break' },
+        ];
+      }
+      return [...prev, item];
+    }, []);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove "OR" operator in specified position.
+   * @param targetId Remove the break operator after the target one.
+   */
+  const removeOrOp = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+  };
+  /**
+   * Add new condition to specified position.
+   * @param targetId Position where new condition item will be inserted.
+   * Will be pushed to tail if empty.
+   * @param beforeTarget Will be inserted before the target item.
+   */
+  const addCondition = (targetId?: string, beforeTarget?: boolean) => {
+    const formerConditons = [...flatConditions];
+    const newItem = {
+      id: generateIdByHash('condition'),
+      type: 'condition',
+      field: '',
+      operator: '<',
+      value: '',
+    };
+    if (!targetId) {
+      formerConditons.push(newItem);
+      setFilteredFlatConditions(formerConditons);
+      return;
+    }
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        const newItems = [
+          item,
+          {
+            id: generateIdByHash('condition'),
+            type: 'condition',
+            field: '',
+            operator: '<',
+            value: '',
+          },
+        ];
+        beforeTarget && newItems.reverse();
+        return [...prev, ...newItems];
+      }
+      return [...prev, item];
+    }, []);
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove condition from specified position.
+   * @param targetId Position where new condition item will be removed.
+   */
+  const removeCondition = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+  };
+  const changeBinaryLogicalOp = (value: string, targetId: string) => {
+    if (value === 'or') {
+      addOrOp(targetId);
+    } else if (value === 'and') {
+      removeOrOp(targetId);
+    }
+  };
+  /**
+   * Update specified condition's data.
+   * @param id Specify one item that will be updated.
+   * @param data Data that will be updated to condition.
+   */
+  const updateConditionData = (id: string, data: ConditionData) => {
+    const formerFlatConditions = flatConditions.map(i => {
+      if (i.id === id) return { ...i, data };
+      return i;
+    });
+    setFilteredFlatConditions(formerFlatConditions);
+  };
+  // Reset conditions.
+  const resetConditions = () => {
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions([
+      {
+        id: generateIdByHash(),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const getConditionsSum = () => {
+    const conds = flatConditions.filter(i => i.type === 'condition');
+    return conds.length;
+  };
+
+  const handleConditions = {
+    addOrOp,
+    removeOrOp,
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    updateConditionData,
+    resetConditions,
+    getConditionsSum,
+    setFilteredFlatConditions,
+    setIsConditionsLegal,
+    setFilterExpression,
+  };
+
+  const handleClickOpen = () => {
+    setOpen(true);
+  };
+  const handleClose = () => {
+    setOpen(false);
+  };
+  const handleDeleteAll = () => {
+    setFlatConditions([]);
+    setInitConditions([]);
+    onSubmit('');
+  };
+  const handleCancel = () => {
+    setOpen(false);
+    handleFallback();
+  };
+  const handleSubmit = () => {
+    onSubmit(filterExpression);
+    setInitConditions(flatConditions);
+    setOpen(false);
+  };
+  const handleReset = () => {
+    setFilteredFlatConditions([
+      {
+        id: generateIdByHash('condition'),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const handleFallback = () => {
+    setFilteredFlatConditions(initConditions);
+  };
+
+  return (
+    <>
+      <div className={`${classes.wrapper} ${className}`} {...others}>
+        <Button className={`${classes.afBtn} af-btn`} onClick={handleClickOpen}>
+          <FilterListIcon />
+          {showTitle ? title : ''}
+        </Button>
+        {conditionSum > 0 && (
+          <Tooltip
+            arrow
+            interactive
+            title={filterExpression}
+            placement={tooltipPlacement}
+          >
+            <Chip
+              label={conditionSum}
+              onDelete={handleDeleteAll}
+              variant="outlined"
+              size="small"
+            />
+          </Tooltip>
+        )}
+        {open && (
+          <AdvancedDialog
+            open={open}
+            onClose={handleClose}
+            onCancel={handleCancel}
+            onSubmit={handleSubmit}
+            onReset={handleReset}
+            title="Advanced Filter"
+            fields={fields}
+            handleConditions={handleConditions}
+            conditions={flatConditions}
+            isLegal={isConditionsLegal}
+            expression={filterExpression}
+          />
+        )}
+      </div>
+    </>
+  );
+};
+
+Filter.displayName = 'AdvancedFilter';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    wrapper: {},
+    afBtn: {
+      color: '#06AFF2',
+    },
+  })
+);
+
+export default Filter;

+ 82 - 0
client/src/components/advancedSearch/Types.ts

@@ -0,0 +1,82 @@
+// import { ReactElement } from 'react';
+
+export interface ConditionProps {
+  others?: object;
+  onDelete: () => void;
+  triggerChange: (id: string, data: TriggerChangeData) => void;
+  fields: Field[];
+  id: string;
+  initData: any;
+  className?: string;
+}
+
+export interface Field {
+  name: string;
+  type: 'int' | 'float';
+}
+
+export interface TriggerChangeData {
+  field: Field;
+  op: string;
+  value: string;
+  isCorrect: boolean;
+  id: string;
+}
+
+export interface ConditionGroupProps {
+  others?: object;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+}
+
+export interface BinaryLogicalOpProps {
+  onChange: (newOp: string) => void;
+  className?: string;
+  initValue?: string;
+}
+
+export interface AddConditionProps {
+  className?: string;
+  onClick?: () => void;
+}
+
+export interface CopyButtonProps {
+  className?: string;
+  icon?: any;
+  label: string;
+  value: string;
+  others?: any;
+}
+
+export interface DialogProps {
+  others?: object;
+  open: boolean;
+  onClose: () => void;
+  onSubmit: (data: any) => void;
+  onReset: () => void;
+  onCancel: () => void;
+  title: string;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+  isLegal: boolean;
+  expression: string;
+}
+
+export interface FilterProps {
+  className?: string;
+  title: string;
+  showTitle?: boolean;
+  others?: object;
+  onSubmit: (data: any) => void;
+  tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';
+  fields: Field[];
+}
+
+export interface ConditionData {
+  field: Field;
+  op: string;
+  value: string;
+  isCorrect: boolean;
+}

+ 3 - 0
client/src/components/advancedSearch/index.tsx

@@ -0,0 +1,3 @@
+import Filter from './Filter';
+
+export default Filter;

+ 18 - 13
client/src/components/customDialog/DialogTemplate.tsx

@@ -13,6 +13,7 @@ import CustomButton from '../customButton/CustomButton';
 const useStyles = makeStyles((theme: Theme) => ({
   actions: {
     paddingTop: theme.spacing(2),
+    justifyContent: 'space-between',
   },
 }));
 
@@ -28,6 +29,7 @@ const DialogTemplate: FC<DialogContainerProps> = ({
   showActions = true,
   showCancel = true,
   showCloseIcon = true,
+  leftActions,
 }) => {
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
@@ -43,20 +45,23 @@ const DialogTemplate: FC<DialogContainerProps> = ({
       <DialogContent>{children}</DialogContent>
       {showActions && (
         <DialogActions className={classes.actions}>
-          {showCancel && (
-            <CustomButton onClick={onCancel} color="default" name="cancel">
-              {cancel}
+          <div>{leftActions}</div>
+          <div>
+            {showCancel && (
+              <CustomButton onClick={onCancel} color="default" name="cancel">
+                {cancel}
+              </CustomButton>
+            )}
+            <CustomButton
+              variant="contained"
+              onClick={handleConfirm}
+              color="primary"
+              disabled={confirmDisabled}
+              name="confirm"
+            >
+              {confirm}
             </CustomButton>
-          )}
-          <CustomButton
-            variant="contained"
-            onClick={handleConfirm}
-            color="primary"
-            disabled={confirmDisabled}
-            name="confirm"
-          >
-            {confirm}
-          </CustomButton>
+          </div>
         </DialogActions>
       )}
     </>

+ 1 - 0
client/src/components/customDialog/Types.ts

@@ -31,4 +31,5 @@ export type DialogContainerProps = {
   confirmDisabled?: boolean;
   showActions?: boolean;
   showCancel?: boolean;
+  leftActions?: ReactElement;
 };

+ 4 - 2
client/src/components/grid/ToolBar.tsx

@@ -69,6 +69,8 @@ const CustomToolBar: FC<ToolBarType> = props => {
 
             const Icon = c.icon ? Icons[c.icon!]() : '';
             const disabled = c.disabled ? c.disabled(selected) : false;
+            // when disabled "disabledTooltip" will replace "tooltip"
+            const tooltip = disabled && c.disabledTooltip ? c.disabledTooltip : c.tooltip;
             const isIcon = c.type === 'iconBtn';
 
             const btn = (
@@ -81,7 +83,7 @@ const CustomToolBar: FC<ToolBarType> = props => {
                 disabled={disabled}
                 // use contained variant as default
                 variant={c.btnVariant || 'contained'}
-                tooltip={c.tooltip}
+                tooltip={tooltip}
                 className={classes.btn}
               >
                 <Typography variant="button">{c.label}</Typography>
@@ -92,7 +94,7 @@ const CustomToolBar: FC<ToolBarType> = props => {
               <CustomIconButton
                 key={i}
                 onClick={c.onClick}
-                tooltip={c.tooltip}
+                tooltip={tooltip}
                 disabled={disabled}
               >
                 {Icon}

+ 2 - 0
client/src/components/grid/Types.ts

@@ -36,6 +36,8 @@ export type ToolBarConfig = Partial<TableSwitchType> &
     onClick?: (arg0: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
     disabled?: (data: any[]) => boolean;
     tooltip?: string;
+    // when disabled "disabledTooltip" will replace "tooltip"
+    disabledTooltip?: string;
     hidden?: boolean;
     type?: 'iconBtn' | 'buttton' | 'switch' | 'select' | 'groupSelect';
     position?: 'right' | 'left';

+ 4 - 0
client/src/components/icons/Icons.tsx

@@ -29,6 +29,7 @@ import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
 import { ReactComponent as UploadIcon } from '../../assets/icons/upload.svg';
+import { ReactComponent as CopyIcon } from '../../assets/icons/copy.svg';
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
@@ -78,6 +79,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   upload: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={UploadIcon} {...props} />
   ),
+  copyExpression: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={CopyIcon} {...props} />
+  ),
 };
 
 export default icons;

+ 2 - 1
client/src/components/icons/Types.ts

@@ -26,4 +26,5 @@ export type IconsType =
   | 'remove'
   | 'key'
   | 'upload'
-  | 'dropdown';
+  | 'dropdown'
+  | 'copyExpression';

+ 41 - 26
client/src/consts/Milvus.tsx

@@ -1,11 +1,11 @@
 export enum METRIC_TYPES_VALUES {
   L2 = 'L2',
   IP = 'IP',
-  HAMMING = 'Hamming',
-  JACCARD = 'Jaccard',
-  TANIMOTO = 'Tanimoto',
-  SUBSTRUCTURE = 'Substructure',
-  SUPERSTRUCTURE = 'Superstructure',
+  HAMMING = 'HAMMING',
+  JACCARD = 'JACCARD',
+  TANIMOTO = 'TANIMOTO',
+  SUBSTRUCTURE = 'SUBSTRUCTURE',
+  SUPERSTRUCTURE = 'SUPERSTRUCTURE',
 }
 
 export const METRIC_TYPES = [
@@ -19,44 +19,46 @@ export const METRIC_TYPES = [
   },
   {
     value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
-    label: 'Substructure',
+    label: 'SUBSTRUCTURE',
   },
   {
     value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
-    label: 'Superstructure',
+    label: 'SUPERSTRUCTURE',
   },
   {
     value: METRIC_TYPES_VALUES.HAMMING,
-    label: 'Hamming',
+    label: 'HAMMING',
   },
   {
     value: METRIC_TYPES_VALUES.JACCARD,
-    label: 'Jaccard',
+    label: 'JACCARD',
   },
   {
     value: METRIC_TYPES_VALUES.TANIMOTO,
-    label: 'Tanimoto',
+    label: 'TANIMOTO',
   },
 ];
 
 export type MetricType =
   | 'L2'
   | 'IP'
-  | 'Hamming'
-  | 'Substructure'
-  | 'Superstructure'
-  | 'Jaccard'
-  | 'Tanimoto';
+  | 'HAMMING'
+  | 'SUBSTRUCTURE'
+  | 'SUPERSTRUCTURE'
+  | 'JACCARD'
+  | 'TANIMOTO';
 
 export type searchKeywordsType = 'nprobe' | 'ef' | 'search_k' | 'search_length';
 
-// index
-export const INDEX_CONFIG: {
+export type indexConfigType = {
   [x: string]: {
     create: string[];
     search: searchKeywordsType[];
   };
-} = {
+}
+
+// index
+export const FLOAT_INDEX_CONFIG: indexConfigType = {
   IVF_FLAT: {
     create: ['nlist'],
     search: ['nprobe'],
@@ -88,7 +90,23 @@ export const INDEX_CONFIG: {
   // RNSG: {
   //   create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
   //   search: ['search_length'],
-  // },
+  // },}
+}
+
+export const BINARY_INDEX_CONFIG: indexConfigType = {
+  BIN_FLAT: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+  BIN_IVF_FLAT: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+};
+
+export const INDEX_CONFIG: indexConfigType = {
+  ...FLOAT_INDEX_CONFIG,
+  ...BINARY_INDEX_CONFIG,
 };
 
 export const COLLECTION_NAME_REGX = /^[0-9,a-z,A-Z$_]+$/;
@@ -102,16 +120,13 @@ export const m_OPTIONS = [
 ];
 
 export const INDEX_OPTIONS_MAP = {
-  FLOAT_POINT: Object.keys(INDEX_CONFIG).map(v => ({ label: v, value: v })),
-  BINARY: [
-    { label: 'FLAT', value: 'FLAT' },
-    { label: 'IVF_FLAT', value: 'IVF_FLAT' },
-  ],
+  FLOAT_INDEX: Object.keys(FLOAT_INDEX_CONFIG).map(v => ({ label: v, value: v })),
+  BINARY_INDEX: Object.keys(BINARY_INDEX_CONFIG).map(v => ({ label: v, value: v })),
 };
 
 export const PRIMARY_KEY_FIELD = 'INT64 (Primary key)';
 
 export enum EmbeddingTypeEnum {
-  float = 'FLOAT_POINT',
-  binary = 'BINARY',
+  float = 'FLOAT_INDEX',
+  binary = 'BINARY_INDEX',
 }

+ 3 - 1
client/src/context/Auth.tsx

@@ -11,12 +11,14 @@ export const authContext = createContext<AuthContextType>({
 
 const { Provider } = authContext;
 export const AuthProvider = (props: { children: React.ReactNode }) => {
+  // get milvus address from local storage
   const [address, setAddress] = useState<string>(
     window.localStorage.getItem(MILVUS_ADDRESS) || ''
   );
   const isAuth = useMemo(() => !!address, [address]);
 
   useEffect(() => {
+    // check if the milvus is still available
     const check = async () => {
       const milvusAddress = window.localStorage.getItem(MILVUS_ADDRESS) || '';
       if (!milvusAddress) {
@@ -38,7 +40,7 @@ export const AuthProvider = (props: { children: React.ReactNode }) => {
 
   useEffect(() => {
     document.title = address
-      ? `Milvus Insight - ${address} `
+      ? `${address} - Milvus Insight`
       : 'Milvus Insight';
   }, [address]);
 

+ 4 - 1
client/src/hooks/Form.ts

@@ -51,7 +51,10 @@ export const useFormValidation = (form: IForm[]): IValidationInfo => {
   // validation detail about form item
   const [validation, setValidation] = useState(initValidation);
   // overall validation result to control following actions
-  const [disabled, setDisabled] = useState<boolean>(true);
+  const isOverallValid = Object.values(validation).every(
+    v => !(v as IValidationItem).result
+  );
+  const [disabled, setDisabled] = useState<boolean>(!isOverallValid);
 
   const checkIsValid = (param: ICheckValidParam): IValidationItem => {
     const { value, key, rules } = param;

+ 1 - 0
client/src/i18n/cn/collection.ts

@@ -7,6 +7,7 @@ const collectionTrans = {
 
   create: 'Create Collection',
   delete: 'delete',
+  deleteTooltip: 'Please select at least one item to delete.',
 
   collection: 'Collection',
 

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

@@ -7,6 +7,7 @@ const collectionTrans = {
 
   create: 'Create Collection',
   delete: 'delete',
+  deleteTooltip: 'Please select at least one item to delete.',
 
   collection: 'Collection',
 

+ 2 - 0
client/src/pages/collections/Collections.tsx

@@ -308,6 +308,8 @@ const Collections = () => {
       },
       label: collectionTrans('delete'),
       icon: 'delete',
+      // tooltip: collectionTrans('deleteTooltip'),
+      disabledTooltip: collectionTrans('deleteTooltip'),
       disabled: data => data.length === 0,
     },
     {

+ 29 - 1
client/src/pages/connect/Connect.tsx

@@ -4,7 +4,7 @@ import { ITextfieldConfig } from '../../components/customInput/Types';
 import icons from '../../components/icons/Icons';
 import ConnectContainer from './ConnectContainer';
 import CustomInput from '../../components/customInput/CustomInput';
-import { useContext, useMemo, useState } from 'react';
+import { useContext, useEffect, useMemo, useState } from 'react';
 import { formatForm } from '../../utils/Form';
 import { useFormValidation } from '../../hooks/Form';
 import CustomButton from '../../components/customButton/CustomButton';
@@ -14,6 +14,7 @@ import { MilvusHttp } from '../../http/Milvus';
 import { rootContext } from '../../context/Root';
 import { MILVUS_ADDRESS } from '../../consts/Localstorage';
 import { formatAddress } from '../../utils/Format';
+// import { io } from "socket.io-client";
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -99,6 +100,33 @@ const Connect = () => {
     defaultValue: form.address,
   };
 
+  // test code for socket
+  // useEffect(() => {
+  //   const socket = io('http://localhost:3002');
+  //   socket.on('connect', function () {
+  //     console.log('Connected');
+
+  //     socket.emit('identity', 0, (res: any) =>
+  //       console.log(res));
+
+  //     socket.emit('events', { test: 'events' });
+
+  //     socket.emit('senddata', { test: 'senddata' });
+  //   });
+  //   socket.on('events', (data: any) => {
+  //     console.log('event', data);
+  //   });
+  //   socket.on('senddata', (data: any) => {
+  //     console.log('senddata', data);
+  //   });
+  //   socket.on('exception', (data: any) => {
+  //     console.log('event', data);
+  //   });
+  //   socket.on('disconnect', () => {
+  //     console.log('Disconnected');
+  //   });
+  // }, []);
+
   return (
     <ConnectContainer>
       <section className={classes.wrapper}>

+ 9 - 5
client/src/pages/schema/Create.tsx

@@ -6,12 +6,13 @@ import {
   INDEX_CONFIG,
   INDEX_OPTIONS_MAP,
   MetricType,
+  METRIC_TYPES_VALUES,
 } from '../../consts/Milvus';
 import { useFormValidation } from '../../hooks/Form';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { DataType } from '../collections/Types';
 import CreateForm from './CreateForm';
-import { IndexType, ParamPair } from './Types';
+import { IndexType, ParamPair, INDEX_TYPES_ENUM } from './Types';
 
 const CreateIndex = (props: {
   collectionName: string;
@@ -25,14 +26,15 @@ const CreateIndex = (props: {
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: btnTrans } = useTranslation('btn');
 
-  const defaultMetricType = fieldType === 'BinaryVector' ? 'Hamming' : 'L2';
+  const defaultIndexType = fieldType === 'BinaryVector' ? INDEX_TYPES_ENUM.BIN_IVF_FLAT : INDEX_TYPES_ENUM.IVF_FLAT;
+  const defaultMetricType = fieldType === 'BinaryVector' ? METRIC_TYPES_VALUES.HAMMING : METRIC_TYPES_VALUES.L2;
 
   const [indexSetting, setIndexSetting] = useState<{
     index_type: IndexType;
     metric_type: MetricType;
     [x: string]: string;
   }>({
-    index_type: 'IVF_FLAT',
+    index_type: defaultIndexType,
     metric_type: defaultMetricType,
     M: '',
     m: '4',
@@ -87,7 +89,8 @@ const CreateIndex = (props: {
 
   // reset index params
   useEffect(() => {
-    setDisabled(true);
+    // no need
+    // setDisabled(true);
     setIndexSetting(v => ({
       ...v,
       metric_type: defaultMetricType,
@@ -115,7 +118,8 @@ const CreateIndex = (props: {
       .forEach(item => {
         paramsForm[item] = '';
       });
-
+    // if no other params, the form should be valid.
+    setDisabled((INDEX_CONFIG[type].create || []).length === 0 ? false : true);
     const form = formatForm(paramsForm);
     resetValidation(form);
   };

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

@@ -216,6 +216,7 @@ const Schema: FC<{
         rowsPerPage={pageSize}
         setRowsPerPage={handlePageSize}
         isLoading={loading}
+        openCheckBox={false}
       />
     </section>
   );

+ 10 - 5
client/src/pages/schema/Types.ts

@@ -10,6 +10,8 @@ export enum INDEX_TYPES_ENUM {
   HNSW = 'HNSW',
   ANNOY = 'ANNOY',
   RNSG = 'RNSG',
+  BIN_IVF_FLAT = 'BIN_IVF_FLAT',
+  BIN_FLAT = 'BIN_FLAT',
 }
 
 export interface Field {
@@ -48,14 +50,17 @@ export interface IndexView {
 }
 
 export type IndexType =
-  | 'FLAT'
-  | 'IVF_FLAT'
+  | INDEX_TYPES_ENUM.FLAT
+  | INDEX_TYPES_ENUM.IVF_FLAT
   // | 'IVF_SQ8'
   // | 'IVF_SQ8_HYBRID'
-  | 'IVF_PQ'
+  | INDEX_TYPES_ENUM.IVF_PQ
   // | 'RNSG'
-  | 'HNSW'
-  | 'ANNOY';
+  | INDEX_TYPES_ENUM.HNSW
+  | INDEX_TYPES_ENUM.ANNOY
+  | INDEX_TYPES_ENUM.BIN_IVF_FLAT
+  | INDEX_TYPES_ENUM.BIN_FLAT
+
 
 export interface IndexManageParam {
   collection_name: string;

+ 17 - 0
client/src/utils/Common.ts

@@ -44,3 +44,20 @@ export const findKeyValue = (
   obj: { key: string; value: string }[],
   key: string
 ) => obj.find(v => v.key === key)?.value;
+
+export const generateHashCode = (source: string) => {
+  let hash = 0,
+    i,
+    chr;
+  if (source.length === 0) return hash;
+  for (i = 0; i < source.length; i++) {
+    chr = source.charCodeAt(i);
+    hash = (hash << 5) - hash + chr;
+    hash |= 0; // Convert to 32bit integer
+  }
+  return hash.toString();
+};
+
+export const generateIdByHash = (salt?: string) => {
+  return generateHashCode(`${new Date().getTime().toString()}-${salt}`);
+};

+ 7 - 7
client/src/utils/Form.ts

@@ -38,15 +38,15 @@ export const getMetricOptions = (
   const baseBinaryOptions = [
     {
       value: METRIC_TYPES_VALUES.HAMMING,
-      label: 'Hamming',
+      label: 'HAMMING',
     },
     {
       value: METRIC_TYPES_VALUES.JACCARD,
-      label: 'Jaccard',
+      label: 'JACCARD',
     },
     {
       value: METRIC_TYPES_VALUES.TANIMOTO,
-      label: 'Tanimoto',
+      label: 'TANIMOTO',
     },
   ];
 
@@ -54,18 +54,18 @@ export const getMetricOptions = (
 
   const baseOptionsMap: { [key: string]: any } = {
     BinaryVector: {
-      FLAT: [
+      BIN_FLAT: [
         ...baseBinaryOptions,
         {
           value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
-          label: 'Substructure',
+          label: 'SUBSTRUCTURE',
         },
         {
           value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
-          label: 'Superstructure',
+          label: 'SUPERSTRUCTURE',
         },
       ],
-      IVF_FLAT: baseBinaryOptions,
+      BIN_IVF_FLAT: baseBinaryOptions,
     },
     FloatVector: {
       ALL: baseFloatOptions,

+ 88 - 2
client/yarn.lock

@@ -1831,6 +1831,11 @@
   dependencies:
     "@babel/types" "^7.3.0"
 
+"@types/component-emitter@^1.2.10":
+  version "1.2.10"
+  resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
+  integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==
+
 "@types/eslint@^7.2.6":
   version "7.2.13"
   resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.13.tgz#e0ca7219ba5ded402062ad6f926d491ebb29dd53"
@@ -2935,11 +2940,21 @@ babylon@^6.18.0:
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
   integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
 
+backo2@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+  integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
+
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-arraybuffer@0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
+  integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
+
 base64-js@^1.0.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -3599,7 +3614,7 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
 
-component-emitter@^1.2.1:
+component-emitter@^1.2.1, component-emitter@~1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
   integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
@@ -4141,6 +4156,13 @@ debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
   dependencies:
     ms "^2.1.1"
 
+debug@~4.3.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
+  dependencies:
+    ms "2.1.2"
+
 decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -4504,6 +4526,28 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
   dependencies:
     once "^1.4.0"
 
+engine.io-client@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.1.2.tgz#27108da9b39ae03262443d945caf2caa3655c4cb"
+  integrity sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==
+  dependencies:
+    base64-arraybuffer "0.1.4"
+    component-emitter "~1.3.0"
+    debug "~4.3.1"
+    engine.io-parser "~4.0.1"
+    has-cors "1.1.0"
+    parseqs "0.0.6"
+    parseuri "0.0.6"
+    ws "~7.4.2"
+    yeast "0.1.2"
+
+engine.io-parser@~4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
+  integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
+  dependencies:
+    base64-arraybuffer "0.1.4"
+
 enhanced-resolve@^4.3.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec"
@@ -5541,6 +5585,11 @@ has-bigints@^1.0.1:
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
   integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
 
+has-cors@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
+  integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
+
 has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -8177,6 +8226,16 @@ parse5@6.0.1:
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
+parseqs@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
+  integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
+
+parseuri@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
+  integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
+
 parseurl@~1.3.2, parseurl@~1.3.3:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -10251,6 +10310,28 @@ snapdragon@^0.8.1:
     source-map-resolve "^0.5.0"
     use "^3.1.0"
 
+socket.io-client@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.3.tgz#236daa642a9f229932e00b7221e843bf74232a62"
+  integrity sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==
+  dependencies:
+    "@types/component-emitter" "^1.2.10"
+    backo2 "~1.0.2"
+    component-emitter "~1.3.0"
+    debug "~4.3.1"
+    engine.io-client "~5.1.2"
+    parseuri "0.0.6"
+    socket.io-parser "~4.0.4"
+
+socket.io-parser@~4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
+  integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
+  dependencies:
+    "@types/component-emitter" "^1.2.10"
+    component-emitter "~1.3.0"
+    debug "~4.3.1"
+
 sockjs-client@^1.5.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6"
@@ -11751,7 +11832,7 @@ ws@^6.2.1:
   dependencies:
     async-limiter "~1.0.0"
 
-ws@^7.4.5:
+ws@^7.4.5, ws@~7.4.2:
   version "7.4.6"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
   integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
@@ -11840,6 +11921,11 @@ yargs@^15.4.1:
     y18n "^4.0.0"
     yargs-parser "^18.1.2"
 
+yeast@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
+  integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
+
 yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"

+ 5 - 0
server/package.json

@@ -26,12 +26,15 @@
     "@nestjs/jwt": "^7.2.0",
     "@nestjs/passport": "^7.1.5",
     "@nestjs/platform-express": "^7.6.15",
+    "@nestjs/platform-socket.io": "^8.0.4",
     "@nestjs/serve-static": "^2.1.4",
     "@nestjs/swagger": "^4.8.0",
+    "@nestjs/websockets": "^8.0.4",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-local": "^1.0.33",
     "@zilliz/milvus2-sdk-node": "^1.0.3",
     "body-parser": "^1.19.0",
+    "cache-manager": "^3.4.4",
     "class-transformer": "^0.4.0",
     "class-validator": "^0.13.1",
     "passport": "^0.4.1",
@@ -40,12 +43,14 @@
     "reflect-metadata": "^0.1.13",
     "rimraf": "^3.0.2",
     "rxjs": "^6.6.6",
+    "socket.io": "^4.1.3",
     "swagger-ui-express": "^4.1.6"
   },
   "devDependencies": {
     "@nestjs/cli": "^7.6.0",
     "@nestjs/schematics": "^7.3.0",
     "@nestjs/testing": "^7.6.15",
+    "@types/cache-manager": "^3.4.2",
     "@types/express": "^4.17.11",
     "@types/jest": "^26.0.22",
     "@types/node": "^14.14.36",

+ 5 - 0
server/src/cache/config.ts

@@ -0,0 +1,5 @@
+export const ttl = 10; //seconds
+export const cacheKeys = {
+  LOADEDCOLLECTIONS: 'LOADEDCOLLECTIONS',
+  ALLCOLLECTIONS: 'ALLCOLLECTIONS',
+};

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

@@ -9,7 +9,12 @@ import {
   Query,
   UsePipes,
   ValidationPipe,
+  CACHE_MANAGER,
+  Inject,
+  UseInterceptors,
+  CacheInterceptor,
 } from '@nestjs/common';
+import { Cache } from 'cache-manager';
 import { ApiTags } from '@nestjs/swagger';
 import { CollectionsService } from './collections.service';
 import {
@@ -18,20 +23,38 @@ import {
   ShowCollections,
   VectorSearch,
 } from './dto';
+import { cacheKeys } from '../cache/config';
 
+//Including 2 kind of cache check getCollections and getStatistics for detail
 @ApiTags('collections')
 @Controller('collections')
 export class CollectionsController {
-  constructor(private collectionsService: CollectionsService) {}
+  constructor(private collectionsService: CollectionsService, @Inject(CACHE_MANAGER) private cacheManager: Cache) { }
 
+  // manually control cache if logic is complicated
   @Get()
   async getCollections(@Query() data?: ShowCollections) {
-    return Number(data.type) === 1
-      ? await this.collectionsService.getLoadedColletions()
-      : await this.collectionsService.getAllCollections();
+    if (Number(data.type) === 1) {
+      let loadedCollections = await this.cacheManager.get(cacheKeys.LOADEDCOLLECTIONS);
+      if (loadedCollections) {
+        return loadedCollections;
+      }
+      loadedCollections = await this.collectionsService.getLoadedColletions();
+      await this.cacheManager.set(cacheKeys.LOADEDCOLLECTIONS, loadedCollections);
+      return loadedCollections;
+    }
+    let allCollections = await this.cacheManager.get(cacheKeys.ALLCOLLECTIONS);
+    if (allCollections) {
+      return allCollections;
+    }
+    allCollections = await this.collectionsService.getAllCollections();
+    await this.cacheManager.set(cacheKeys.ALLCOLLECTIONS, allCollections);
+    return allCollections;
   }
 
+  // use interceptor to control cache automatically
   @Get('statistics')
+  @UseInterceptors(CacheInterceptor)
   async getStatistics() {
     return await this.collectionsService.getStatistics();
   }
@@ -39,12 +62,14 @@ export class CollectionsController {
   @Post()
   @UsePipes(new ValidationPipe())
   async createCollection(@Body() data: CreateCollection) {
+    await this.cacheManager.del(cacheKeys.ALLCOLLECTIONS);
     return await this.collectionsService.createCollection(data);
   }
 
   @Delete(':name')
   // todo: need check some special symbols
   async deleteCollection(@Param('name') name: string) {
+    await this.cacheManager.del(cacheKeys.ALLCOLLECTIONS);
     return await this.collectionsService.dropCollection({
       collection_name: name,
     });
@@ -71,6 +96,7 @@ export class CollectionsController {
 
   @Put(':name/load')
   async loadCollection(@Param('name') name: string) {
+    await this.cacheManager.del(cacheKeys.LOADEDCOLLECTIONS);
     return await this.collectionsService.loadCollection({
       collection_name: name,
     });
@@ -78,6 +104,7 @@ export class CollectionsController {
 
   @Put(':name/release')
   async releaseCollection(@Param('name') name: string) {
+    await this.cacheManager.del(cacheKeys.LOADEDCOLLECTIONS);
     return await this.collectionsService.releaseCollection({
       collection_name: name,
     });
@@ -85,6 +112,7 @@ export class CollectionsController {
 
   @Post(':name/insert')
   async insertData(@Param('name') name: string, @Body() data: InsertData) {
+    await this.cacheManager.del(cacheKeys.ALLCOLLECTIONS);
     return await this.collectionsService.insert({
       collection_name: name,
       ...data,

+ 9 - 3
server/src/collections/collections.module.ts

@@ -1,11 +1,17 @@
-import { Module } from '@nestjs/common';
+import { Module, CacheModule } from '@nestjs/common';
 import { CollectionsService } from './collections.service';
 import { CollectionsController } from './collections.controller';
 import { MilvusModule } from '../milvus/milvus.module';
+import { ttl } from '../cache/config';
 
 @Module({
-  imports: [MilvusModule],
+  imports: [
+    MilvusModule,
+    CacheModule.register({
+      ttl, // seconds
+    }),
+  ],
   providers: [CollectionsService],
   controllers: [CollectionsController],
 })
-export class CollectionsModule {}
+export class CollectionsModule { }

+ 42 - 0
server/src/events/events.gateway.ts

@@ -0,0 +1,42 @@
+import {
+  MessageBody,
+  SubscribeMessage,
+  WebSocketGateway,
+  WebSocketServer,
+  WsResponse,
+} from '@nestjs/websockets';
+import { from, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { Server } from 'socket.io';
+
+@WebSocketGateway(3002, {
+  cors: {
+    origin: "*",
+    methods: ["GET", "POST"],
+  }
+})
+export class EventsGateway {
+  @WebSocketServer()
+  server: Server;
+
+  @SubscribeMessage('senddata')
+  data(@MessageBody() data: unknown): WsResponse<unknown> {
+    const event = 'senddata';
+    return { event, data };
+  }
+
+  @SubscribeMessage('events')
+  event(@MessageBody() data: any): Observable<WsResponse<number>> {
+    const event = 'events';
+    const response = [1, 2, 3];
+
+    return from(response).pipe(
+      map(data => ({ event, data })),
+    );
+  }
+
+  @SubscribeMessage('identity')
+  async identity(@MessageBody() data: any): Promise<string> {
+    return `identity data: ${data}`;
+  }
+}

+ 7 - 0
server/src/events/events.module.ts

@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common';
+import { EventsGateway } from './events.gateway';
+
+@Module({
+  providers: [EventsGateway],
+})
+export class EventsModule {}

文件差异内容过多而无法显示
+ 329 - 245
server/yarn.lock


部分文件因为文件数量过多而无法显示