Browse Source

Merge pull request #1 from nameczz/feature/client-init

init client
nameczz 3 years ago
parent
commit
5bea9b66f4
100 changed files with 6386 additions and 1 deletions
  1. 46 1
      client/README.md
  2. 52 0
      client/package.json
  3. BIN
      client/public/favicon.ico
  4. BIN
      client/public/favicon.png
  5. 43 0
      client/public/index.html
  6. BIN
      client/public/logo192.png
  7. BIN
      client/public/logo512.png
  8. 25 0
      client/public/manifest.json
  9. 3 0
      client/public/robots.txt
  10. 12 0
      client/src/App.tsx
  11. BIN
      client/src/assets/imgs/logo.png
  12. 31 0
      client/src/components/__test__/copy/Copy.spec.tsx
  13. 39 0
      client/src/components/__test__/customButton/customButton.spec.tsx
  14. 99 0
      client/src/components/__test__/customDialog/CustomDialog.spec.tsx
  15. 121 0
      client/src/components/__test__/customSelector/CustomGroupedSelect.spec.tsx
  16. 44 0
      client/src/components/__test__/customSnackBar/CustomSnackBar.spec.tsx
  17. 48 0
      client/src/components/__test__/customToolTip/CustomToolTip.spec.tsx
  18. 49 0
      client/src/components/__test__/filter/Filter.spec.tsx
  19. 54 0
      client/src/components/__test__/grid/IconBtnCell.spec.tsx
  20. 163 0
      client/src/components/__test__/grid/Table.spec.tsx
  21. 126 0
      client/src/components/__test__/grid/TableHead.spec.tsx
  22. 95 0
      client/src/components/__test__/grid/Toolbar.spec.tsx
  23. 51 0
      client/src/components/__test__/grid/Utils.spec.ts
  24. 137 0
      client/src/components/__test__/grid/index.spec.tsx
  25. 37 0
      client/src/components/__test__/layout/GlobalEffect.spec.tsx
  26. 32 0
      client/src/components/__test__/layout/Layout.spec.tsx
  27. 48 0
      client/src/components/__test__/menu/SimpleMenu.spec.tsx
  28. 55 0
      client/src/components/__test__/status/Status.spec.tsx
  29. 190 0
      client/src/components/__test__/textField/customInput.spec.tsx
  30. 54 0
      client/src/components/copy/Copy.tsx
  31. 85 0
      client/src/components/customButton/CustomButton.tsx
  32. 21 0
      client/src/components/customButton/CustomIconButton.tsx
  33. 179 0
      client/src/components/customCard/CustomCard.tsx
  34. 19 0
      client/src/components/customCard/Types.ts
  35. 120 0
      client/src/components/customDialog/CustomDialog.tsx
  36. 52 0
      client/src/components/customDialog/CustomDialogTitle.tsx
  37. 20 0
      client/src/components/customDialog/Types.ts
  38. 102 0
      client/src/components/customSelector/CustomGroupedSelect.tsx
  39. 54 0
      client/src/components/customSelector/CustomSelector.tsx
  40. 31 0
      client/src/components/customSelector/Types.ts
  41. 82 0
      client/src/components/customSnackBar/CustomSnackBar.tsx
  42. 2 0
      client/src/components/customSnackBar/Types.ts
  43. 101 0
      client/src/components/customTabList/CustomTabList.tsx
  44. 19 0
      client/src/components/customTabList/Types.ts
  45. 29 0
      client/src/components/customToolTip/CustomToolTip.tsx
  46. 22 0
      client/src/components/customToolTip/Types.ts
  47. 162 0
      client/src/components/filter/Filter.tsx
  48. 5 0
      client/src/components/filter/Types.ts
  49. 57 0
      client/src/components/grid/ActionBar.tsx
  50. 41 0
      client/src/components/grid/LoadingTable.tsx
  51. 291 0
      client/src/components/grid/Table.tsx
  52. 109 0
      client/src/components/grid/TableHead.tsx
  53. 83 0
      client/src/components/grid/TablePaginationActions.tsx
  54. 59 0
      client/src/components/grid/TableSwitch.tsx
  55. 175 0
      client/src/components/grid/ToolBar.tsx
  56. 135 0
      client/src/components/grid/Types.ts
  57. 36 0
      client/src/components/grid/Utils.ts
  58. 234 0
      client/src/components/grid/index.tsx
  59. 33 0
      client/src/components/icons/Icons.tsx
  60. 14 0
      client/src/components/icons/Types.ts
  61. 44 0
      client/src/components/layout/GlobalEffect.tsx
  62. 105 0
      client/src/components/layout/GlobalToolbar.tsx
  63. 70 0
      client/src/components/layout/Header.tsx
  64. 60 0
      client/src/components/layout/Layout.tsx
  65. 5 0
      client/src/components/layout/Types.ts
  66. 37 0
      client/src/components/layout/UserContainer.tsx
  67. 186 0
      client/src/components/menu/NavMenu.tsx
  68. 71 0
      client/src/components/menu/SimpleMenu.tsx
  69. 25 0
      client/src/components/menu/Types.ts
  70. 89 0
      client/src/components/status/Status.tsx
  71. 44 0
      client/src/components/status/StatusIcon.tsx
  72. 8 0
      client/src/components/status/Types.ts
  73. 306 0
      client/src/components/textField/CustomInput.tsx
  74. 142 0
      client/src/components/textField/SearchInput.tsx
  75. 100 0
      client/src/components/textField/Types.ts
  76. 7 0
      client/src/consts/Http.ts
  77. 3 0
      client/src/consts/Localstorage.ts
  78. 141 0
      client/src/consts/Milvus.tsx
  79. 6 0
      client/src/consts/Polling.ts
  80. 6 0
      client/src/consts/Reducer.ts
  81. 6 0
      client/src/consts/Util.ts
  82. 1 0
      client/src/consts/WebSocket.ts
  83. 281 0
      client/src/context/Root.tsx
  84. 52 0
      client/src/context/Types.ts
  85. 37 0
      client/src/hooks/CommonStyle.ts
  86. 129 0
      client/src/hooks/Form.ts
  87. 22 0
      client/src/hooks/Pagination.ts
  88. 29 0
      client/src/hooks/Polling.ts
  89. 29 0
      client/src/hooks/__test__/CommonStyle.spec.tsx
  90. 55 0
      client/src/hooks/__test__/Form.spec.tsx
  91. 35 0
      client/src/http/Axios.ts
  92. 25 0
      client/src/i18n/cn/button.ts
  93. 13 0
      client/src/i18n/cn/card.ts
  94. 31 0
      client/src/i18n/cn/collection.ts
  95. 77 0
      client/src/i18n/cn/common.ts
  96. 12 0
      client/src/i18n/cn/connect.ts
  97. 5 0
      client/src/i18n/cn/data.ts
  98. 43 0
      client/src/i18n/cn/database.ts
  99. 8 0
      client/src/i18n/cn/dialog.ts
  100. 15 0
      client/src/i18n/cn/help.ts

+ 46 - 1
client/README.md

@@ -1 +1,46 @@
-Client
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `yarn start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+### `yarn test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `yarn build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `yarn eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+
+If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
+
+You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).

+ 52 - 0
client/package.json

@@ -0,0 +1,52 @@
+{
+  "name": "milvus-admin",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@material-ui/core": "^4.11.4",
+    "@material-ui/icons": "^4.11.2",
+    "@material-ui/lab": "^4.0.0-alpha.58",
+    "@testing-library/jest-dom": "^5.11.4",
+    "@testing-library/react": "^11.1.0",
+    "@testing-library/user-event": "^12.1.10",
+    "@types/jest": "^26.0.15",
+    "@types/node": "^12.0.0",
+    "@types/react": "^17.0.0",
+    "@types/react-dom": "^17.0.0",
+    "@types/react-router-dom": "^5.1.7",
+    "axios": "^0.21.1",
+    "dayjs": "^1.10.5",
+    "i18next": "^20.3.1",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2",
+    "react-i18next": "^11.10.0",
+    "react-router-dom": "^5.2.0",
+    "react-scripts": "4.0.3",
+    "typescript": "^4.1.2",
+    "web-vitals": "^1.0.1"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}

BIN
client/public/favicon.ico


BIN
client/public/favicon.png


+ 43 - 0
client/public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.png" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>Milvus Admin</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

BIN
client/public/logo192.png


BIN
client/public/logo512.png


+ 25 - 0
client/public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
client/public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 12 - 0
client/src/App.tsx

@@ -0,0 +1,12 @@
+import Router from './router/Router';
+import { RootProvider } from './context/Root';
+
+function App() {
+  return (
+    <RootProvider>
+      <Router></Router>
+    </RootProvider>
+  );
+}
+
+export default App;

BIN
client/src/assets/imgs/logo.png


+ 31 - 0
client/src/components/__test__/copy/Copy.spec.tsx

@@ -0,0 +1,31 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import Copy from '../../copy/Copy';
+let container: any = null;
+
+jest.mock('@material-ui/core/Tooltip', () => {
+  return props => {
+    return <div id="tooltip">{props.children}</div>;
+  };
+});
+
+describe('Test Copy Component', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test props ', () => {
+    act(() => {
+      render(<Copy data={[]}></Copy>, container);
+    });
+
+    expect(document.querySelectorAll('button').length).toEqual(1);
+  });
+});

+ 39 - 0
client/src/components/__test__/customButton/customButton.spec.tsx

@@ -0,0 +1,39 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import CustomButton from '../../customButton/CustomButton';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/Button', () => {
+  return props => {
+    const { variant, children } = props;
+    return (
+      <>
+        <div className="variant">{variant}</div>
+        <button className="button">{children}</button>;
+      </>
+    );
+  };
+});
+
+describe('Test CustomButton', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  test('test button props', () => {
+    act(() => {
+      render(<CustomButton variant="contained">test</CustomButton>, container);
+    });
+
+    expect(container.querySelector('.button').textContent).toBe('test');
+    expect(container.querySelector('.variant').textContent).toBe('contained');
+  });
+});

+ 99 - 0
client/src/components/__test__/customDialog/CustomDialog.spec.tsx

@@ -0,0 +1,99 @@
+import { fireEvent } from '@testing-library/react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import { DialogType } from '../../../context/Types';
+import CustomDialog from '../../customDialog/CustomDialog';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/Dialog', () => {
+  return props => {
+    return <div id="dialog-wrapper">{props.children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/DialogTitle', () => {
+  return props => {
+    return <div id="dialog-title">{props.children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/DialogContent', () => {
+  return props => {
+    return <div id="dialog-content">{props.children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/DialogActions', () => {
+  return props => {
+    return <div id="dialog-actions">{props.children}</div>;
+  };
+});
+
+describe('Test Custom Dialog', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test notice dialog ', () => {
+    const handleClose = jest.fn();
+    const handleConfirm = jest.fn();
+
+    const params: DialogType = {
+      open: true,
+      type: 'notice',
+      params: {
+        title: 'delete',
+        confirm: handleConfirm,
+        component: <div>123</div>,
+      },
+    };
+    act(() => {
+      render(
+        <CustomDialog {...params} onClose={handleClose}></CustomDialog>,
+        container
+      );
+    });
+
+    expect(container.querySelector('#dialog-title').textContent).toEqual(
+      params.params.title
+    );
+
+    expect(container.querySelector('#dialog-content').textContent).toEqual(
+      '123'
+    );
+
+    container.querySelectorAll('button').forEach(v => fireEvent.click(v));
+    expect(handleClose).toBeCalledTimes(1);
+    expect(handleConfirm).toBeCalledTimes(1);
+  });
+
+  it('Test Custom dialog ', () => {
+    const handleClose = jest.fn();
+
+    const params: DialogType = {
+      open: true,
+      type: 'custom',
+      params: {
+        component: <div>custom</div>,
+      },
+    };
+    act(() => {
+      render(
+        <CustomDialog {...params} onClose={handleClose}></CustomDialog>,
+        container
+      );
+    });
+
+    expect(container.querySelector('#dialog-wrapper').textContent).toEqual(
+      'custom'
+    );
+  });
+});

+ 121 - 0
client/src/components/__test__/customSelector/CustomGroupedSelect.spec.tsx

@@ -0,0 +1,121 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import { fireEvent } from '@testing-library/react';
+import CustomGroupedSelect from '../../customSelector/CustomGroupedSelect';
+import { GroupOption } from '../../customSelector/Types';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/FormControl', () => {
+  return props => {
+    const { children } = props;
+    return <div className="form-control">{children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/Select', () => {
+  return props => {
+    const { children, onChange } = props;
+    return (
+      <select className="group-select" onChange={onChange}>
+        {children}
+      </select>
+    );
+  };
+});
+
+jest.mock('@material-ui/core/ListSubheader', () => {
+  return props => {
+    const { children } = props;
+    return <option className="group-header">{children}</option>;
+  };
+});
+
+jest.mock('@material-ui/core/MenuItem', () => {
+  return props => {
+    const { children, value } = props;
+    return (
+      <option className="group-item" value={value}>
+        {children}
+      </option>
+    );
+  };
+});
+
+describe('Test CustomGroupedSelect', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  test('test group select props', () => {
+    const mockOptions: GroupOption[] = [
+      {
+        label: 'Group 1',
+        children: [
+          {
+            label: 'group text 1',
+            value: 'group text 1',
+          },
+          {
+            label: 'group text 2',
+            value: 'group text 2',
+          },
+          {
+            label: 'group text 3',
+            value: 'group text 3',
+          },
+        ],
+      },
+      {
+        label: 'Group 2',
+        children: [
+          {
+            label: 'group text 11',
+            value: 'group text 11',
+          },
+          {
+            label: 'group text 22',
+            value: 'group text 22',
+          },
+          {
+            label: 'group text 33',
+            value: 'group text 33',
+          },
+        ],
+      },
+    ];
+    const handleChange = jest.fn();
+
+    act(() => {
+      render(
+        <CustomGroupedSelect
+          options={mockOptions}
+          value={''}
+          onChange={handleChange}
+        />,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('.form-control').length).toBe(1);
+    expect(container.querySelectorAll('.group-header').length).toBe(2);
+    expect(container.querySelectorAll('.group-item').length).toBe(6);
+
+    const select = container.querySelector('.group-select');
+
+    fireEvent.change(select, {
+      target: {
+        value: 'group text 2',
+      },
+    });
+    expect(handleChange).toHaveBeenCalledTimes(1);
+    expect(select.value).toBe('group text 2');
+  });
+});

+ 44 - 0
client/src/components/__test__/customSnackBar/CustomSnackBar.spec.tsx

@@ -0,0 +1,44 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import { SnackBarType } from '../../../context/Types';
+import CustomSnackBar from '../../customSnackBar/CustomSnackBar';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/Snackbar', () => {
+  return props => {
+    return <div id="snackbar">{props.children}</div>;
+  };
+});
+
+describe('Test Custom Dialog', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test Custom dialog ', () => {
+    const params: SnackBarType = {
+      open: false,
+      type: 'success',
+      message: 'test',
+      vertical: 'top',
+      horizontal: 'center',
+      autoHideDuration: 2000,
+    };
+    const handleClose = jest.fn();
+    act(() => {
+      render(<CustomSnackBar {...params} onClose={handleClose} />, container);
+    });
+
+    expect(container.querySelector('#snackbar').textContent).toEqual(
+      params.message
+    );
+  });
+});

+ 48 - 0
client/src/components/__test__/customToolTip/CustomToolTip.spec.tsx

@@ -0,0 +1,48 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import CustomToolTip from '../../customToolTip/CustomToolTip';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/Tooltip', () => {
+  return props => {
+    return (
+      <div id="tooltip">
+        <div id="title">{props.title}</div>
+        <div id="placement">{props.placement}</div>
+        {props.children}
+      </div>
+    );
+  };
+});
+
+describe('Test Tool Tip', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test props ', () => {
+    act(() => {
+      render(
+        <CustomToolTip title="test" placement="right-end">
+          <div id="children">child</div>
+        </CustomToolTip>,
+        container
+      );
+    });
+
+    expect(container.querySelector('#title').textContent).toEqual('test');
+    expect(container.querySelector('#placement').textContent).toEqual(
+      'right-end'
+    );
+
+    expect(container.querySelector('#children').textContent).toEqual('child');
+  });
+});

+ 49 - 0
client/src/components/__test__/filter/Filter.spec.tsx

@@ -0,0 +1,49 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import Filter from '../../filter/Filter';
+import { fireEvent } from '@testing-library/react';
+
+let container: any = null;
+
+// jest.mock('../../customButton/CustomButton', () => {
+//   return props => {
+//     <button>{props.children}</button>;
+//   };
+// });
+
+jest.mock('@material-ui/core/styles/makeStyles', () => {
+  return () => () => ({});
+});
+
+describe('Test Filter', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  test('test filter props', () => {
+    const options = [
+      { label: 'a', value: 1 },
+      { label: 'b', value: 2 },
+    ];
+    const filterSpy = jest.fn();
+    act(() => {
+      render(
+        <Filter
+          filterOptions={options}
+          filterTitle="test"
+          onFilter={filterSpy}
+        />,
+        container
+      );
+    });
+    fireEvent.click(container.querySelector('button'));
+    expect(container.querySelector('p').textContent).toEqual('test');
+  });
+});

+ 54 - 0
client/src/components/__test__/grid/IconBtnCell.spec.tsx

@@ -0,0 +1,54 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import ActionBar from '../../grid/ActionBar';
+import { fireEvent } from '@testing-library/react';
+
+let container: any = null;
+
+describe('Test Table Head', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test No Button', () => {
+    act(() => {
+      render(<ActionBar configs={[]} row={{ a: 1 }} />, container);
+    });
+
+    expect(container.querySelectorAll('.icon-btn').length).toEqual(0);
+  });
+
+  it('Test Delete Icon Button', () => {
+    const deleteSpy = jest.fn();
+    const showDialogSpy = jest.fn();
+
+    act(() => {
+      render(
+        <ActionBar
+          row={{ a: 1 }}
+          configs={[
+            { onClick: deleteSpy, icon: 'delete', label: 'delete' },
+            { onClick: showDialogSpy, icon: 'list', label: 'dialog' },
+          ]}
+        />,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('button').length).toEqual(2);
+    expect(deleteSpy).toBeCalledTimes(0);
+
+    fireEvent.click(container.querySelector('button[aria-label="delete"]'));
+    expect(deleteSpy).toBeCalledTimes(1);
+
+    fireEvent.click(container.querySelector('button[aria-label="dialog"]'));
+    expect(showDialogSpy).toBeCalledTimes(1);
+  });
+});

+ 163 - 0
client/src/components/__test__/grid/Table.spec.tsx

@@ -0,0 +1,163 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import Table from '../../grid/Table';
+import { ColDefinitionsType } from '../../grid/Types';
+
+let container: any = null;
+
+const colDefinitions: ColDefinitionsType[] = [
+  {
+    id: 'id',
+    disablePadding: true,
+    label: 'ID',
+  },
+  {
+    id: 'name',
+    disablePadding: false,
+    label: 'Name',
+  },
+  {
+    id: 'action',
+    disablePadding: false,
+    label: 'Action',
+    showActionCell: true,
+    actionBarConfigs: [
+      {
+        onClick: () => {},
+        icon: 'delete',
+        label: 'delete',
+      },
+    ],
+  },
+];
+
+jest.mock('@material-ui/core/styles/makeStyles', () => {
+  return () => () => ({});
+});
+
+jest.mock('../../grid/LoadingTable.tsx', () => {
+  return () => {
+    return <div className="loading"></div>;
+  };
+});
+
+describe('Test Table', () => {
+  let data: any[] = [];
+  let onSelected: any;
+  let isSelected: any;
+  let onSelectedAll: any;
+
+  beforeEach(() => {
+    data = [
+      {
+        id: 1,
+        name: 'czz',
+      },
+    ];
+    onSelected = jest.fn();
+    isSelected = jest.fn().mockImplementation(() => true);
+    onSelectedAll = jest.fn();
+
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test Basic Table', () => {
+    act(() => {
+      render(
+        <Table
+          selected={[]}
+          onSelected={onSelected}
+          isSelected={isSelected}
+          onSelectedAll={onSelectedAll}
+          rows={data}
+          primaryKey="id"
+          colDefinitions={colDefinitions}
+        ></Table>,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('input').length).toEqual(2); // check box
+    expect(container.querySelector('tr').children.length).toEqual(
+      colDefinitions.length + 1
+    );
+    expect(container.querySelectorAll('th')[1].textContent).toEqual(
+      colDefinitions[0].label
+    );
+    expect(container.querySelectorAll('th')[2].textContent).toEqual(
+      colDefinitions[1].label
+    );
+
+    expect(container.querySelectorAll('[aria-label="delete"]').length).toEqual(
+      1
+    );
+  });
+
+  it('Test Selected function', () => {
+    act(() => {
+      render(
+        <Table
+          selected={[1]}
+          onSelected={onSelected}
+          isSelected={isSelected}
+          onSelectedAll={onSelectedAll}
+          rows={data}
+          primaryKey="id"
+          colDefinitions={colDefinitions}
+        ></Table>,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('input')[1].checked).toBeTruthy();
+    expect(container.querySelectorAll('input')[0].checked).toBeTruthy();
+
+    isSelected = jest.fn().mockImplementation(() => false);
+    act(() => {
+      render(
+        <Table
+          selected={[]}
+          onSelected={onSelected}
+          isSelected={isSelected}
+          onSelectedAll={onSelectedAll}
+          rows={data}
+          primaryKey="id"
+          colDefinitions={colDefinitions}
+        ></Table>,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('input')[1].checked).toBeFalsy();
+    expect(container.querySelectorAll('input')[0].checked).toBeFalsy();
+
+    expect(isSelected).toHaveBeenCalledTimes(1);
+  });
+
+  it('Test Table Loading status', () => {
+    act(() => {
+      render(
+        <Table
+          selected={[]}
+          onSelected={onSelected}
+          isSelected={isSelected}
+          onSelectedAll={onSelectedAll}
+          rows={data}
+          primaryKey="id"
+          colDefinitions={colDefinitions}
+          isLoading={true}
+        ></Table>,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('.loading').length).toBe(1);
+  });
+});

+ 126 - 0
client/src/components/__test__/grid/TableHead.spec.tsx

@@ -0,0 +1,126 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import TableHead from '../../grid/TableHead';
+import { fireEvent } from '@testing-library/react';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/TableHead', () => {
+  return (props: any) => {
+    return <div id="table-head">{props.children}</div>;
+  };
+});
+jest.mock('@material-ui/core/TableRow', () => {
+  return (props: any) => {
+    return <div id="table-row">{props.children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/TableCell', () => {
+  return (props: any) => {
+    return <div className="table-cell">{props.children}</div>;
+  };
+});
+
+describe('Test Table Head', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test no checkbox', () => {
+    act(() => {
+      render(
+        <TableHead
+          colDefinitions={[]}
+          numSelected={0}
+          order={'desc'}
+          orderBy={'id'}
+          onSelectAllClick={() => {}}
+          onRequestSort={() => {}}
+          rowCount={0}
+          openCheckBox={false}
+        />,
+        container
+      );
+    });
+    expect(container.querySelectorAll('.table-cell').length).toEqual(0);
+  });
+
+  it('Test checkbox open', () => {
+    const selectAllSpy = jest.fn();
+    act(() => {
+      render(
+        <TableHead
+          colDefinitions={[]}
+          numSelected={10}
+          order={'desc'}
+          orderBy={'id'}
+          onSelectAllClick={selectAllSpy}
+          onRequestSort={() => {}}
+          rowCount={10}
+          openCheckBox={true}
+        />,
+        container
+      );
+    });
+
+    const checkboxDom = container.querySelector('input[type="checkbox"]');
+    expect(container.querySelectorAll('.table-cell').length).toEqual(1);
+    expect(checkboxDom).toBeDefined();
+
+    fireEvent.click(checkboxDom);
+    expect(selectAllSpy).toBeCalledTimes(1);
+    expect(checkboxDom.checked).toBeTruthy();
+  });
+
+  it('Test header cells', () => {
+    const onRequestSortSpy = jest.fn();
+    const colDefinitions = [
+      {
+        id: 'id',
+        numeric: false,
+        disablePadding: true,
+        label: 'id',
+      },
+      {
+        id: 'name',
+        numeric: false,
+        disablePadding: true,
+        label: 'name',
+      },
+    ];
+    act(() => {
+      render(
+        <TableHead
+          colDefinitions={colDefinitions}
+          numSelected={10}
+          order={'desc'}
+          orderBy={'id'}
+          onSelectAllClick={() => {}}
+          onRequestSort={onRequestSortSpy}
+          rowCount={10}
+          openCheckBox={false}
+        />,
+        container
+      );
+    });
+
+    const headerCells = container.querySelectorAll('.MuiTableSortLabel-root');
+    expect(headerCells.length).toEqual(colDefinitions.length);
+
+    fireEvent.click(headerCells[0]);
+    expect(onRequestSortSpy).toBeCalledTimes(1);
+
+    fireEvent.click(headerCells[1]);
+    expect(onRequestSortSpy).toBeCalledTimes(2);
+    expect(headerCells[0].textContent).toContain('id');
+    expect(headerCells[0].textContent).toContain('sorted descending');
+  });
+});

+ 95 - 0
client/src/components/__test__/grid/Toolbar.spec.tsx

@@ -0,0 +1,95 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import Toolbar from '../../grid/ToolBar';
+import { ToolBarConfig } from '../../grid/Types';
+
+jest.mock('@material-ui/icons/Search', () => {
+  return () => {
+    return <div id="search">search</div>;
+  };
+});
+
+jest.mock('../../customButton/CustomButton', () => {
+  return () => {
+    return <div className="button">button</div>;
+  };
+});
+
+jest.mock('../../textField/SearchInput', () => {
+  return props => {
+    return <div>{props.children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/TextField', () => {
+  console.log('test enter');
+  return props => {
+    return <input {...props} className="input" />;
+  };
+});
+
+let container: any = null;
+
+const cb = jest.fn().mockImplementation(resolve => resolve('a'));
+
+let toolbarConfig: ToolBarConfig[] = [];
+
+describe('Test ToolBar', () => {
+  beforeEach(() => {
+    toolbarConfig = [
+      {
+        label: 'collection',
+        icon: 'delete',
+        onClick: cb,
+        disabled: selected => selected.length > 1,
+      },
+    ];
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test only one config', () => {
+    act(() => {
+      render(
+        <Toolbar
+          selected={[]}
+          setSelected={() => {}}
+          toolbarConfigs={toolbarConfig}
+        ></Toolbar>,
+        container
+      );
+    });
+
+    const btnDom = container.querySelector('.button');
+    expect(container.querySelectorAll('.button').length).toBe(1);
+    expect(btnDom.className.includes('disabled')).toBeFalsy();
+    expect(container.querySelector('#search')).toBeNull();
+  });
+
+  it('Test Search Config', () => {
+    toolbarConfig.push({
+      label: 'collection',
+      icon: 'search',
+      onClick: cb,
+      onSearch: cb,
+    });
+    act(() => {
+      render(
+        <Toolbar
+          selected={[]}
+          setSelected={() => {}}
+          toolbarConfigs={toolbarConfig}
+        ></Toolbar>,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('.input').length).toBe(0);
+  });
+});

+ 51 - 0
client/src/components/__test__/grid/Utils.spec.ts

@@ -0,0 +1,51 @@
+import {
+  descendingComparator,
+  getComparator,
+  stableSort,
+} from '../../grid/Utils';
+
+describe('Test Gird Utils', () => {
+  it('Test descendingComparator', () => {
+    const a = {
+      order: 2,
+    };
+
+    const b = {
+      order: 11,
+    };
+    expect(descendingComparator(a, b, 'order')).toEqual(1);
+  });
+
+  it('Test getComparator', () => {
+    const a = {
+      order: 2,
+    };
+
+    const b = {
+      order: 11,
+    };
+    expect(getComparator('desc', 'order')(a, b)).toEqual(1);
+    expect(getComparator('asc', 'order')(a, b)).toEqual(-1);
+  });
+
+  it('Test stableSort', () => {
+    const arr = [
+      {
+        order: 2,
+      },
+      {
+        order: 11,
+      },
+    ];
+
+    const comparator = getComparator('desc', 'order');
+    expect(stableSort(JSON.parse(JSON.stringify(arr)), comparator)).toEqual(
+      JSON.parse(JSON.stringify(arr)).reverse()
+    );
+
+    const ascComparator = getComparator('asc', 'order');
+    expect(stableSort(JSON.parse(JSON.stringify(arr)), ascComparator)).toEqual(
+      arr
+    );
+  });
+});

+ 137 - 0
client/src/components/__test__/grid/index.spec.tsx

@@ -0,0 +1,137 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import MilvusGrid from '../../grid/index';
+import { ToolBarConfig } from '../../grid/Types';
+
+let container: any = null;
+
+jest.mock('react-i18next', () => {
+  return {
+    useTranslation: () => ({
+      t: () => ({
+        grid: {},
+      }),
+    }),
+  };
+});
+
+jest.mock('../../grid/Table', () => {
+  return () => {
+    return <div id="table">{}</div>;
+  };
+});
+
+jest.mock('../../grid/ToolBar', () => {
+  return () => {
+    return <div id="tool-bar"></div>;
+  };
+});
+
+jest.mock('react-router-dom', () => {
+  return {
+    useHistory: () => {
+      return {
+        listen: () => () => {},
+        location: {
+          name: '',
+        },
+      };
+    },
+  };
+});
+
+describe('Test Grid index', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Has Table Data', () => {
+    act(() => {
+      render(
+        <MilvusGrid
+          primaryKey="id"
+          rows={[{}]}
+          colDefinitions={[]}
+          rowCount={10}
+          toolbarConfigs={[]}
+        />,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('#table').length).toEqual(1);
+  });
+
+  it('Test title', () => {
+    const title = ['collections', 'vectors'];
+    act(() => {
+      render(
+        <MilvusGrid
+          primaryKey="id"
+          rows={[]}
+          colDefinitions={[]}
+          rowCount={0}
+          toolbarConfigs={[]}
+          title={title}
+        />,
+        container
+      );
+    });
+
+    const titleNodes = container.querySelectorAll('h6');
+    expect(titleNodes.length).toEqual(title.length);
+    expect(titleNodes[0].textContent).toEqual(title[0]);
+    expect(titleNodes[1].textContent).toEqual(title[1]);
+  });
+
+  it('Test SearchForm', () => {
+    const SearchForm = () => <div id="search-form"></div>;
+    act(() => {
+      render(
+        <MilvusGrid
+          primaryKey="id"
+          rows={[]}
+          colDefinitions={[]}
+          rowCount={0}
+          toolbarConfigs={[]}
+          searchForm={<SearchForm />}
+        />,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('#search-form').length).toEqual(1);
+  });
+
+  it('Test Toolbar ', () => {
+    const ToolbarConfig: ToolBarConfig[] = [
+      {
+        label: 'collection',
+        icon: 'search',
+        onClick: () => {},
+        onSearch: () => {},
+      },
+    ];
+    act(() => {
+      render(
+        <MilvusGrid
+          primaryKey="id"
+          rows={[]}
+          colDefinitions={[]}
+          rowCount={0}
+          toolbarConfigs={ToolbarConfig}
+        />,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('#tool-bar').length).toEqual(1);
+  });
+});

+ 37 - 0
client/src/components/__test__/layout/GlobalEffect.spec.tsx

@@ -0,0 +1,37 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import GlobalEffect from '../../layout/GlobalEffect';
+
+let container: any = null;
+
+jest.mock('react-router-dom', () => {
+  return {
+    useHistory: () => ({ location: { pathname: '' } }),
+  };
+});
+
+describe('Test GlobalEffect', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test Render', () => {
+    act(() => {
+      render(
+        <GlobalEffect>
+          <div id="children"></div>
+        </GlobalEffect>,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('#children').length).toEqual(1);
+  });
+});

+ 32 - 0
client/src/components/__test__/layout/Layout.spec.tsx

@@ -0,0 +1,32 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import Layout from '../../layout/Layout';
+
+let container: any = null;
+
+jest.mock('../../layout/GlobalEffect', () => {
+  return () => {
+    return <div id="global">{}</div>;
+  };
+});
+
+describe('Test Layout', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test Render', () => {
+    act(() => {
+      render(<Layout />, container);
+    });
+
+    expect(container.querySelectorAll('#global').length).toEqual(1);
+  });
+});

+ 48 - 0
client/src/components/__test__/menu/SimpleMenu.spec.tsx

@@ -0,0 +1,48 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import SimpleMenu from '../../menu/SimpleMenu';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/styles/makeStyles', () => {
+  return () => () => ({});
+});
+
+jest.mock('@material-ui/core/MenuItem', () => {
+  return () => {
+    return <div className="menu-item"></div>;
+  };
+});
+
+describe('Test Simple Menu', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test props ', () => {
+    const items = [
+      {
+        label: 'a',
+        callback: () => {},
+      },
+      {
+        label: 'Logout',
+        callback: () => {
+          console.log('logout');
+        },
+      },
+    ];
+    act(() => {
+      render(<SimpleMenu label="test" menuItems={items} />, container);
+    });
+
+    expect(container.querySelector('button').textContent).toEqual('test');
+  });
+});

+ 55 - 0
client/src/components/__test__/status/Status.spec.tsx

@@ -0,0 +1,55 @@
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import Status from '../../status/Status';
+import { StatusEnum } from '../../status/Types';
+
+let container: any = null;
+
+jest.mock('react-i18next', () => {
+  return {
+    useTranslation: () => {
+      return {
+        t: name => {
+          return {
+            creating: 'creating',
+            running: 'running',
+            error: 'error',
+          };
+        },
+      };
+    },
+  };
+});
+
+jest.mock('@material-ui/core/Typography', () => {
+  return props => {
+    return <div className="label">{props.children}</div>;
+  };
+});
+
+describe('Test Status', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  it('Test props status', () => {
+    act(() => {
+      render(<Status status={StatusEnum.creating} />, container);
+    });
+
+    expect(container.querySelector('.label').textContent).toEqual('creating');
+
+    act(() => {
+      render(<Status status={StatusEnum.running} />, container);
+    });
+
+    expect(container.querySelector('.label').textContent).toEqual('running');
+  });
+});

+ 190 - 0
client/src/components/__test__/textField/customInput.spec.tsx

@@ -0,0 +1,190 @@
+import { fireEvent } from '@testing-library/react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import CustomInput from '../../textField/CustomInput';
+import {
+  IAdornmentConfig,
+  IIconConfig,
+  ITextfieldConfig,
+} from '../../textField/Types';
+
+let container: any = null;
+
+jest.mock('@material-ui/core/FormControl', () => {
+  return props => {
+    const { children } = props;
+    return <div className="form-control">{children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/InputLabel', () => {
+  return props => {
+    return <div className="label">{props.children}</div>;
+  };
+});
+
+jest.mock('@material-ui/core/Input', () => {
+  return props => {
+    const { type, onBlur, endAdornment } = props;
+    return (
+      <>
+        <div className="type">{type}</div>
+        <input className="input" type={type} onBlur={onBlur} />
+        <div>{endAdornment}</div>
+      </>
+    );
+  };
+});
+
+jest.mock('@material-ui/core/TextField', () => {
+  return props => {
+    const { helperText, onBlur, onChange, label, className } = props;
+    return (
+      <div className="text-field">
+        <div className="text-class">{className}</div>
+        <div className="text-label">{label}</div>
+        <div className="text-error">{helperText}</div>
+        <input
+          className="text-input"
+          onBlur={onBlur}
+          onChange={onChange}
+          type="text"
+        />
+      </div>
+    );
+  };
+});
+
+jest.mock('@material-ui/core/Grid', () => {
+  return props => {
+    const { children } = props;
+    return <div className="grid">{children}</div>;
+  };
+});
+
+describe('Test CustomInput', () => {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+  });
+
+  afterEach(() => {
+    unmountComponentAtNode(container);
+    container.remove();
+    container = null;
+  });
+
+  test('test text type input', () => {
+    const handleBlur = jest.fn();
+
+    const mockTextConfig: ITextfieldConfig = {
+      variant: 'standard',
+      label: 'test text',
+      key: 'text',
+      className: 'classname',
+      onBlur: handleBlur,
+    };
+
+    act(() => {
+      render(
+        <CustomInput
+          type="text"
+          textConfig={mockTextConfig}
+          checkValid={() => true}
+        />,
+        container
+      );
+    });
+
+    expect(container.querySelectorAll('.text-field').length).toBe(1);
+    expect(container.querySelector('.text-class').textContent).toBe(
+      'classname'
+    );
+    expect(container.querySelector('.text-label').textContent).toBe(
+      'test text'
+    );
+
+    const input = container.querySelector('.text-input');
+    input.focus();
+    input.blur();
+    expect(handleBlur).toHaveBeenCalledTimes(1);
+  });
+
+  test('test icon type input', () => {
+    const handleChange = jest.fn();
+
+    const mockIconConfig: IIconConfig = {
+      icon: <div className="icon"></div>,
+      inputType: 'icon',
+      inputConfig: {
+        label: 'icon text',
+        key: 'icon',
+        onChange: handleChange,
+        variant: 'standard',
+      },
+    };
+
+    render(
+      <CustomInput
+        type="icon"
+        iconConfig={mockIconConfig}
+        checkValid={() => true}
+      />,
+      container
+    );
+
+    expect(container.querySelectorAll('.grid').length).toBe(3);
+    expect(container.querySelectorAll('.icon').length).toBe(1);
+    expect(container.querySelector('.text-label').textContent).toBe(
+      'icon text'
+    );
+
+    const input = container.querySelector('.text-input');
+    fireEvent.change(input, { target: { value: 'trigger change' } });
+    expect(handleChange).toHaveBeenCalledTimes(1);
+  });
+
+  test('test adornmentConfig type input', () => {
+    const mockBlurFunc = jest.fn();
+
+    const mockAdornmentConfig: IAdornmentConfig = {
+      label: 'adornment',
+      icon: <div className="adornment-icon"></div>,
+      isPasswordType: false,
+      key: 'adornment',
+      onInputBlur: mockBlurFunc,
+    };
+
+    render(
+      <CustomInput
+        type="adornment"
+        adornmentConfig={mockAdornmentConfig}
+        checkValid={() => true}
+      />,
+      container
+    );
+
+    expect(container.querySelector('.label').textContent).toBe('adornment');
+    expect(container.querySelector('.type').textContent).toBe('text');
+    expect(container.querySelectorAll('.adornment-icon').length).toBe(1);
+
+    const input = container.querySelector('.input');
+    input.focus();
+    input.blur();
+    expect(mockBlurFunc).toHaveBeenCalledTimes(1);
+  });
+
+  test('test default type input', () => {
+    const mockTextConfig: ITextfieldConfig = {
+      label: 'default',
+      key: 'default',
+      variant: 'standard',
+    };
+
+    act(() => {
+      render(<CustomInput textConfig={mockTextConfig} />, container);
+    });
+
+    expect(container.querySelector('.text-label').textContent).toBe('default');
+  });
+});

+ 54 - 0
client/src/components/copy/Copy.tsx

@@ -0,0 +1,54 @@
+import { IconButton, makeStyles } from '@material-ui/core';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { copyToCommand } from '../../utils/Common';
+import CustomToolTip from '../customToolTip/CustomToolTip';
+import Icons from '../icons/Icons';
+
+const useStyles = makeStyles(theme => ({
+  copy: {
+    cursor: 'pointer',
+    '& svg': {
+      fontSize: '12.8px',
+    },
+  },
+}));
+
+let timer: null | NodeJS.Timeout = null;
+const Copy = (props: { data: any }) => {
+  const classes = useStyles();
+  const { data } = props;
+  const { t } = useTranslation();
+  const copyTrans = t('copy') as any;
+  const [title, setTitle] = useState(copyTrans.copy);
+
+  const handleCopy = (
+    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
+    data: string
+  ) => {
+    if (timer) {
+      clearTimeout(timer);
+    }
+    e.stopPropagation();
+
+    const cb = () => {
+      setTitle(copyTrans.copied);
+      setTimeout(() => {
+        setTitle(copyTrans.copy);
+      }, 1100);
+    };
+    timer = setTimeout(() => {
+      copyToCommand(data, '', cb);
+    }, 200);
+  };
+
+  return (
+    <CustomToolTip leaveDelay={900} title={title} placement="top">
+      <IconButton className={classes.copy} onClick={e => handleCopy(e, data)}>
+        {Icons.copy()}
+      </IconButton>
+    </CustomToolTip>
+  );
+};
+
+export default Copy;

+ 85 - 0
client/src/components/customButton/CustomButton.tsx

@@ -0,0 +1,85 @@
+import { Button, ButtonProps, makeStyles, Tooltip } from '@material-ui/core';
+
+const buttonStyle = makeStyles(theme => ({
+  button: {
+    padding: theme.spacing(1, 3),
+    textTransform: 'initial',
+    fontWeight: 'bold',
+  },
+  textBtn: {
+    color: theme.palette.primary.main,
+    padding: theme.spacing(1),
+
+    '&:hover': {
+      backgroundColor: '#f9f9f9',
+    },
+  },
+  containedBtn: {
+    color: '#fff',
+    backgroundColor: theme.palette.primary.main,
+    boxShadow: 'initial',
+    fontWeight: 'bold',
+    lineHeight: '16px',
+    '&:hover': {
+      backgroundColor: theme.palette.primary.light,
+      boxShadow: 'initial',
+    },
+  },
+  containedSecondary: {
+    backgroundColor: '#fc4c02',
+
+    '&:hover': {
+      backgroundColor: '#fc4c02',
+    },
+  },
+  disabledBtn: {
+    pointerEvents: 'none',
+  },
+}));
+
+// props types same as Material Button
+const CustomButton = (props: ButtonProps & { tooltip?: string }) => {
+  const classes = buttonStyle();
+  const { tooltip, ...otherProps } = props;
+
+  return (
+    <>
+      {/*
+      add span to let disabled elements show tooltip
+      see https://material-ui.com/zh/components/tooltips/#disabled-elements
+      */}
+      {tooltip ? (
+        <Tooltip title={tooltip}>
+          <span>
+            <Button
+              classes={{
+                root: classes.button,
+                text: classes.textBtn,
+                contained: classes.containedBtn,
+                containedSecondary: classes.containedSecondary,
+                disabled: classes.disabledBtn,
+              }}
+              {...otherProps}
+            >
+              {props.children}
+            </Button>
+          </span>
+        </Tooltip>
+      ) : (
+        <Button
+          classes={{
+            root: classes.button,
+            text: classes.textBtn,
+            contained: classes.containedBtn,
+            containedSecondary: classes.containedSecondary,
+          }}
+          {...otherProps}
+        >
+          {props.children}
+        </Button>
+      )}
+    </>
+  );
+};
+
+export default CustomButton;

+ 21 - 0
client/src/components/customButton/CustomIconButton.tsx

@@ -0,0 +1,21 @@
+import { IconButtonProps, Tooltip, IconButton } from '@material-ui/core';
+
+const CustomIconButton = (props: IconButtonProps & { tooltip?: string }) => {
+  const { tooltip, ...otherProps } = props;
+
+  return (
+    <>
+      {tooltip ? (
+        <Tooltip title={tooltip}>
+          <span>
+            <IconButton {...otherProps}>{props.children}</IconButton>
+          </span>
+        </Tooltip>
+      ) : (
+        <IconButton {...otherProps}>{props.children}</IconButton>
+      )}
+    </>
+  );
+};
+
+export default CustomIconButton;

+ 179 - 0
client/src/components/customCard/CustomCard.tsx

@@ -0,0 +1,179 @@
+import {
+  Card,
+  CardActions,
+  CardContent,
+  CardHeader,
+  IconButton,
+  makeStyles,
+  Menu,
+  MenuItem,
+  Theme,
+  Tooltip,
+} from '@material-ui/core';
+import React, { FC } from 'react';
+import icons from '../icons/Icons';
+import { ICustomCardProps, IMenu } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    boxShadow: 'none',
+    filter: 'drop-shadow(0px 8px 24px rgba(0, 0, 0, 0.1))',
+    width: '100%',
+
+    position: 'relative',
+  },
+  menuItem: {
+    minWidth: '160px',
+    textTransform: 'capitalize',
+
+    '&:hover': {
+      backgroundColor: theme.palette.zilliz.light,
+    },
+  },
+  menuPaper: {
+    boxShadow: '0px 8px 16px rgba(0, 0, 0, 0.15)',
+  },
+  menuContent: {
+    padding: 0,
+
+    '&:last-child': {
+      paddingBottom: 0,
+    },
+  },
+  actions: {
+    '&:hover': {
+      background:
+        'linear-gradient(0deg, rgba(18, 195, 244, 0.05), rgba(18, 195, 244, 0.05)), #fff',
+
+      transition: 'all 0.3s',
+    },
+  },
+
+  actionsDisable: {
+    '&:hover': {
+      backgroundColor: '#fff',
+    },
+  },
+  actionBtn: {
+    color: theme.palette.primary.main,
+  },
+
+  mask: {
+    position: 'absolute',
+    top: 0,
+    right: 0,
+    left: 0,
+    bottom: 0,
+    backgroundColor: 'rgba(196, 196, 196, 0.5)',
+    zIndex: theme.zIndex.modal,
+  },
+}));
+
+const CustomCard: FC<ICustomCardProps> = props => {
+  const {
+    showCardHeaderTitle = true,
+    cardHeaderTitle = '',
+    menu = [],
+    content,
+    actions,
+    wrapperClassName = '',
+    showMask,
+    actionsDisabled = false,
+  } = props;
+  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
+
+  const handleMoreClick = (event: React.MouseEvent<HTMLButtonElement>) => {
+    setAnchorEl(event.currentTarget);
+  };
+
+  const handleMenuClose = () => {
+    setAnchorEl(null);
+  };
+
+  const classes = getStyles();
+
+  const handleClick = (event: any, m: IMenu) => {
+    if (m.onClick) {
+      m.onClick(event);
+    }
+
+    handleMenuClose();
+  };
+
+  return (
+    <Card className={`${classes.root} ${wrapperClassName}`}>
+      {showMask && <div className={classes.mask}></div>}
+      <CardHeader
+        action={
+          menu.length > 0 && (
+            <>
+              <IconButton aria-label="settings" onClick={handleMoreClick}>
+                {icons.more({ classes: { root: classes.actionBtn } })}
+              </IconButton>
+              <Menu
+                anchorEl={anchorEl}
+                disableScrollLock={true}
+                keepMounted
+                open={Boolean(anchorEl)}
+                onClose={handleMenuClose}
+                getContentAnchorEl={null}
+                anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+                transformOrigin={{ vertical: 'top', horizontal: 'center' }}
+                classes={{ paper: classes.menuPaper }}
+              >
+                {menu.map((m, index) =>
+                  typeof m.label === 'string' ? (
+                    m.tip ? (
+                      <Tooltip
+                        key={m.label}
+                        title={m.tip}
+                        placement="right-end"
+                      >
+                        <span style={{ display: 'block' }}>
+                          <MenuItem
+                            classes={{ root: classes.menuItem }}
+                            onClick={event => handleClick(event, m)}
+                            disabled={m.disabled}
+                          >
+                            {m.label}
+                          </MenuItem>
+                        </span>
+                      </Tooltip>
+                    ) : (
+                      <MenuItem
+                        classes={{ root: classes.menuItem }}
+                        key={m.label}
+                        onClick={event => handleClick(event, m)}
+                        disabled={m.disabled}
+                      >
+                        {m.label}
+                      </MenuItem>
+                    )
+                  ) : (
+                    <span key={index}>{m.label}</span>
+                  )
+                )}
+              </Menu>
+            </>
+          )
+        }
+        title={showCardHeaderTitle ? cardHeaderTitle : null}
+      />
+      <CardContent classes={{ root: classes.menuContent }}>
+        {content}
+      </CardContent>
+      {actions && (
+        <CardActions
+          classes={{
+            root: actionsDisabled ? classes.actionsDisable : classes.actions,
+          }}
+          disableSpacing
+        >
+          {actions}
+        </CardActions>
+      )}
+    </Card>
+  );
+};
+
+export default CustomCard;

+ 19 - 0
client/src/components/customCard/Types.ts

@@ -0,0 +1,19 @@
+import { ReactElement } from 'react';
+
+export interface IMenu {
+  label: string | ReactElement;
+  onClick?: (event: any) => void;
+  disabled?: boolean;
+  tip?: string | null;
+}
+
+export interface ICustomCardProps {
+  showCardHeaderTitle?: boolean;
+  cardHeaderTitle?: string | ReactElement;
+  menu?: IMenu[];
+  content: string | ReactElement;
+  actions?: ReactElement;
+  wrapperClassName?: string;
+  showMask?: boolean;
+  actionsDisabled?: boolean;
+}

+ 120 - 0
client/src/components/customDialog/CustomDialog.tsx

@@ -0,0 +1,120 @@
+import React, { FC } from 'react';
+import {
+  DialogActions,
+  DialogContent,
+  Dialog,
+  makeStyles,
+  Theme,
+  createStyles,
+  Typography,
+} from '@material-ui/core';
+import { CustomDialogType } from './Types';
+import { useTranslation } from 'react-i18next';
+import CustomButton from '../customButton/CustomButton';
+import CustomDialogTitle from './CustomDialogTitle';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    paper: {
+      maxWidth: '480px',
+      width: '100%',
+      borderRadius: '0px',
+      padding: 0,
+    },
+    title: {
+      // padding: theme.spacing(4),
+      '& p': {
+        fontWeight: '500',
+        overflow: 'hidden',
+        textOverflow: 'ellipsis',
+        whiteSpace: 'nowrap',
+        maxWidth: '300px',
+        fontSize: '20px',
+      },
+    },
+    padding: {
+      padding: theme.spacing(4),
+    },
+    cancel: {
+      color: theme.palette.common.black,
+      opacity: 0.4,
+    },
+  })
+);
+
+const CustomDialog: FC<CustomDialogType> = props => {
+  const { t } = useTranslation('btn');
+  const classes = useStyles();
+  const { open, type, params, onClose, containerClass = '' } = props;
+  const {
+    title,
+    component,
+    confirm,
+    confirmLabel = t('confirm'),
+    cancel,
+    cancelLabel = t('cancel'),
+    confirmClass = '',
+    handleClose,
+  } = params; // for notice type
+  const { component: CustomComponent } = params; // for custom type
+  const handleConfirm = async () => {
+    if (confirm) {
+      const res = await confirm();
+      if (!res) {
+        return;
+      }
+    }
+    handleClose ? handleClose() : onClose();
+  };
+
+  const handleCancel = async () => {
+    if (cancel) {
+      const res = await cancel();
+      if (!res) {
+        return;
+      }
+    }
+    handleClose ? handleClose() : onClose();
+  };
+
+  return (
+    <Dialog
+      classes={{ paper: classes.paper, container: `${containerClass}` }}
+      open={open}
+      onClose={handleCancel}
+    >
+      {type === 'notice' ? (
+        <>
+          <CustomDialogTitle
+            classes={{ root: classes.title }}
+            onClose={handleCancel}
+          >
+            <Typography variant="body1">{title}</Typography>
+          </CustomDialogTitle>
+          {component && <DialogContent>{component}</DialogContent>}
+          <DialogActions classes={{ spacing: classes.padding }}>
+            <CustomButton
+              onClick={() => handleCancel()}
+              className={classes.cancel}
+              color="default"
+            >
+              {cancelLabel}
+            </CustomButton>
+            <CustomButton
+              onClick={() => handleConfirm()}
+              color="primary"
+              variant="contained"
+              className={confirmClass}
+            >
+              {confirmLabel}
+            </CustomButton>
+          </DialogActions>
+        </>
+      ) : (
+        CustomComponent
+      )}
+    </Dialog>
+  );
+};
+
+export default CustomDialog;

+ 52 - 0
client/src/components/customDialog/CustomDialogTitle.tsx

@@ -0,0 +1,52 @@
+import {
+  DialogTitleProps,
+  makeStyles,
+  Theme,
+  Typography,
+} from '@material-ui/core';
+import MuiDialogTitle from '@material-ui/core/DialogTitle';
+import icons from '../icons/Icons';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    margin: 0,
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  // closeButton: {
+  //   padding: theme.spacing(1),
+  // },
+  icon: {
+    fontSize: '24px',
+    color: '#010e29',
+
+    cursor: 'pointer',
+  },
+}));
+
+interface IProps extends DialogTitleProps {
+  onClose?: () => void;
+}
+
+const CustomDialogTitle = (props: IProps) => {
+  const { children, classes = { root: '' }, onClose, ...other } = props;
+  const innerClass = getStyles();
+
+  const ClearIcon = icons.clear;
+
+  return (
+    <MuiDialogTitle
+      disableTypography
+      className={`${innerClass.root} ${classes.root}`}
+      {...other}
+    >
+      <Typography variant="h5">{children}</Typography>
+      {onClose ? (
+        <ClearIcon classes={{ root: innerClass.icon }} onClick={onClose} />
+      ) : null}
+    </MuiDialogTitle>
+  );
+};
+
+export default CustomDialogTitle;

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

@@ -0,0 +1,20 @@
+import { DialogType } from '../../context/Types';
+export type CustomDialogType = DialogType & {
+  onClose: () => void;
+  containerClass?: string;
+};
+
+export type DeleteDialogType = DeleteDialogContentType & {
+  title: string;
+  open: boolean;
+  setOpen: Function;
+  onDelete: () => void;
+};
+
+export type DeleteDialogContentType = {
+  title: string;
+  text: string;
+  label: string;
+  handleCancel?: () => void;
+  handleDelete: () => void;
+};

+ 102 - 0
client/src/components/customSelector/CustomGroupedSelect.tsx

@@ -0,0 +1,102 @@
+import {
+  FormControl,
+  InputLabel,
+  ListSubheader,
+  makeStyles,
+  MenuItem,
+  Select,
+  Theme,
+} from '@material-ui/core';
+import React, { FC } from 'react';
+import { GroupOption, ICustomGroupSelect } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    width: '100%',
+  },
+  formControl: {
+    width: '100%',
+  },
+  groupName: {
+    paddingLeft: theme.spacing(2),
+    paddingRight: theme.spacing(2),
+    lineHeight: '32px',
+    color: 'rgba(0, 0, 0, 0.33)',
+    fontWeight: 'bold',
+    fontSize: '12.8px',
+  },
+  menuItem: {
+    padding: theme.spacing(0, 4),
+    lineHeight: '24px',
+
+    '&:hover': {
+      backgroundColor: 'rgba(18, 195, 244, 0.05)',
+    },
+  },
+  menuItemSelected: {
+    backgroundColor: 'rgba(0, 0, 0, 0.03)',
+  },
+}));
+
+const CustomGroupedSelect: FC<ICustomGroupSelect> = props => {
+  const classes = getStyles();
+  const {
+    options,
+    className = '',
+    haveLabel = true,
+    label,
+    placeholder = '',
+    value,
+    onChange,
+  } = props;
+
+  const renderSelectGroup = (option: GroupOption) => {
+    const items = option.children.map(child => {
+      return (
+        <MenuItem
+          classes={{ root: classes.menuItem }}
+          key={child.value}
+          value={child.value}
+        >
+          {child.label}
+        </MenuItem>
+      );
+    });
+    return [
+      <ListSubheader key={option.label} classes={{ root: classes.groupName }}>
+        {option.label}
+      </ListSubheader>,
+      items,
+    ];
+  };
+
+  return (
+    <div className={`${classes.wrapper} ${className}`}>
+      <FormControl variant="filled" className={classes.formControl}>
+        {haveLabel && <InputLabel htmlFor="grouped-select">{label}</InputLabel>}
+        <Select
+          displayEmpty={!haveLabel}
+          value={value}
+          id="grouped-select"
+          placeholder={placeholder}
+          onChange={onChange}
+          MenuProps={{
+            anchorOrigin: {
+              vertical: 'bottom',
+              horizontal: 'left',
+            },
+            transformOrigin: {
+              vertical: 'top',
+              horizontal: 'left',
+            },
+            getContentAnchorEl: null,
+          }}
+        >
+          {options.map(option => renderSelectGroup(option))}
+        </Select>
+      </FormControl>
+    </div>
+  );
+};
+
+export default CustomGroupedSelect;

+ 54 - 0
client/src/components/customSelector/CustomSelector.tsx

@@ -0,0 +1,54 @@
+import { FC } from 'react';
+import {
+  createStyles,
+  FormControl,
+  InputLabel,
+  makeStyles,
+  MenuItem,
+  Select,
+  Theme,
+} from '@material-ui/core';
+import { CustomSelectorType } from './Types';
+import { generateId } from '../../utils/Common';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    label: {
+      // textTransform: 'capitalize',
+    },
+  })
+);
+
+/**
+ *  label: We may need label lowecase or capitalize, so we cant control css inside.
+ * */
+const CustomSelector: FC<CustomSelectorType> = props => {
+  const { label, value, onChange, options, classes, variant, ...others } =
+    props;
+  const id = generateId('selector');
+  const selectorClasses = useStyles();
+
+  return (
+    <FormControl variant={variant} classes={classes}>
+      <InputLabel htmlFor={id} className={selectorClasses.label}>
+        {label}
+      </InputLabel>
+      <Select
+        {...others}
+        value={value}
+        onChange={onChange}
+        inputProps={{
+          id,
+        }}
+      >
+        {options.map(v => (
+          <MenuItem key={v.value} value={v.value}>
+            {v.label}
+          </MenuItem>
+        ))}
+      </Select>
+    </FormControl>
+  );
+};
+
+export default CustomSelector;

+ 31 - 0
client/src/components/customSelector/Types.ts

@@ -0,0 +1,31 @@
+import { FormControlClassKey, SelectProps } from '@material-ui/core';
+import { ClassNameMap } from '@material-ui/core/styles/withStyles';
+
+export interface Option {
+  label: string;
+  value: string | number;
+}
+
+export interface GroupOption {
+  label: string;
+  children: Option[];
+}
+
+export type CustomSelectorType = SelectProps & {
+  label: string;
+  value: string | number;
+  options: Option[];
+  onChange: (e: React.ChangeEvent<{ value: unknown }>) => void;
+  classes?: Partial<ClassNameMap<FormControlClassKey>>;
+  variant?: 'filled' | 'outlined' | 'standard';
+};
+
+export interface ICustomGroupSelect {
+  className?: string;
+  options: GroupOption[];
+  haveLabel?: boolean;
+  label?: string;
+  placeholder?: string;
+  value: string | number;
+  onChange: (event: any) => void;
+}

+ 82 - 0
client/src/components/customSnackBar/CustomSnackBar.tsx

@@ -0,0 +1,82 @@
+import React, { FC, SyntheticEvent } from 'react';
+import { CustomSnackBarType } from './Types';
+import MuiAlert from '@material-ui/lab/Alert';
+import { Snackbar, makeStyles, Theme, createStyles } from '@material-ui/core';
+import Slide from '@material-ui/core/Slide';
+import { TransitionProps } from '@material-ui/core/transitions/transition';
+
+// if we need to use slide component
+// snackbar content must use forwardRef to wrapper it
+const Alert = React.forwardRef((props: { [x: string]: any }, ref) => {
+  return <MuiAlert ref={ref} elevation={6} variant="filled" {...props} />;
+});
+
+function SlideTransition(props: TransitionProps) {
+  return <Slide {...props} direction="left" />;
+}
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      borderRadius: '4px',
+      maxWidth: '300px',
+    },
+    topRight: {
+      [theme.breakpoints.up('md')]: {
+        top: '72px',
+        right: theme.spacing(4),
+      },
+      top: '72px',
+      right: theme.spacing(4),
+    },
+  })
+);
+
+const CustomSnackBar: FC<CustomSnackBarType> = props => {
+  const {
+    vertical,
+    horizontal,
+    open,
+    autoHideDuration = 2000,
+    type,
+    message,
+    onClose,
+  } = props;
+  const classes = useStyles();
+  const handleClose = (e: SyntheticEvent<any, Event>, reason: string) => {
+    // only click x to close or auto hide.
+    if (reason === 'clickaway') {
+      return;
+    }
+    onClose && onClose();
+  };
+
+  return (
+    <div>
+      <Snackbar
+        anchorOrigin={{
+          vertical: vertical,
+          horizontal: horizontal,
+        }}
+        key={`${vertical}${horizontal}`}
+        open={open}
+        onClose={handleClose}
+        autoHideDuration={autoHideDuration}
+        classes={{
+          anchorOriginTopRight: classes.topRight,
+        }}
+        TransitionComponent={SlideTransition}
+      >
+        <Alert
+          onClose={handleClose}
+          severity={type}
+          classes={{ root: classes.root }}
+        >
+          {message}
+        </Alert>
+      </Snackbar>
+    </div>
+  );
+};
+
+export default CustomSnackBar;

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

@@ -0,0 +1,2 @@
+import { SnackBarType } from '../../context/Types';
+export type CustomSnackBarType = SnackBarType & { onClose: () => void };

+ 101 - 0
client/src/components/customTabList/CustomTabList.tsx

@@ -0,0 +1,101 @@
+import { Box, makeStyles, Tab, Tabs, Theme } from '@material-ui/core';
+import React, { FC, useState } from 'react';
+import { ITabListProps, ITabPanel } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    '& .MuiTab-wrapper': {
+      textTransform: 'capitalize',
+      fontWeight: 'bold',
+      color: '#323232',
+    },
+  },
+  tab: {
+    height: theme.spacing(0.5),
+    backgroundColor: theme.palette.primary.main,
+  },
+  tabContainer: {
+    borderBottom: '1px solid #e9e9ed',
+  },
+  tabContent: {
+    minWidth: 0,
+    marginRight: theme.spacing(3),
+  },
+  tabPanel: {
+    flexGrow: 1,
+  },
+}));
+
+const TabPanel = (props: ITabPanel) => {
+  const { children, value, index, className = '', ...other } = props;
+
+  return (
+    <div
+      role="tabpanel"
+      hidden={value !== index}
+      className={className}
+      id={`tabpanel-${index}`}
+      aria-labelledby={`tabpanel-${index}`}
+      {...other}
+    >
+      {value === index && <Box height="100%">{children}</Box>}
+    </div>
+  );
+};
+
+const a11yProps = (index: number) => {
+  return {
+    id: `tab-${index}`,
+    'aria-controls': `tabpanel-${index}`,
+  };
+};
+
+const CustomTabList: FC<ITabListProps> = props => {
+  const { tabs, activeIndex = 0, handleTabChange } = props;
+  const classes = useStyles();
+  const [value, setValue] = useState<number>(activeIndex);
+
+  const handleChange = (event: any, newValue: any) => {
+    setValue(newValue);
+
+    handleTabChange && handleTabChange(newValue);
+  };
+
+  return (
+    <>
+      <Tabs
+        classes={{
+          root: classes.wrapper,
+          indicator: classes.tab,
+          flexContainer: classes.tabContainer,
+        }}
+        value={value}
+        onChange={handleChange}
+        aria-label="tabs"
+      >
+        {tabs.map((tab, index) => (
+          <Tab
+            classes={{ root: classes.tabContent }}
+            textColor="primary"
+            key={tab.label}
+            label={tab.label}
+            {...a11yProps(index)}
+          ></Tab>
+        ))}
+      </Tabs>
+
+      {tabs.map((tab, index) => (
+        <TabPanel
+          key={tab.label}
+          value={value}
+          index={index}
+          className={classes.tabPanel}
+        >
+          {tab.component}
+        </TabPanel>
+      ))}
+    </>
+  );
+};
+
+export default CustomTabList;

+ 19 - 0
client/src/components/customTabList/Types.ts

@@ -0,0 +1,19 @@
+import { ReactElement } from 'react';
+
+export interface ITab {
+  label: string;
+  component: ReactElement;
+}
+
+export interface ITabListProps {
+  tabs: ITab[];
+  activeIndex?: number;
+  handleTabChange?: (index: number) => void;
+}
+
+export interface ITabPanel {
+  children: ReactElement | string;
+  value: number;
+  index: number;
+  className?: string;
+}

+ 29 - 0
client/src/components/customToolTip/CustomToolTip.tsx

@@ -0,0 +1,29 @@
+import Tooltip from '@material-ui/core/Tooltip';
+import { CustomToolTipType } from './Types';
+import { FC } from 'react';
+import { makeStyles, Theme, createStyles } from '@material-ui/core';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    tooltip: {
+      textTransform: 'capitalize',
+    },
+  })
+);
+
+const CustomToolTip: FC<CustomToolTipType> = props => {
+  const classes = useStyles();
+  const { title, placement = 'right', children, leaveDelay = 0 } = props;
+  return (
+    <Tooltip
+      classes={{ tooltip: classes.tooltip }}
+      leaveDelay={leaveDelay}
+      title={title}
+      placement={placement}
+    >
+      <span>{children}</span>
+    </Tooltip>
+  );
+};
+
+export default CustomToolTip;

+ 22 - 0
client/src/components/customToolTip/Types.ts

@@ -0,0 +1,22 @@
+import { ReactElement } from 'react';
+
+export type CustomToolTipType = {
+  title: string;
+  placement?: placement;
+  children: ReactElement<any, any>;
+  leaveDelay?: number;
+};
+
+type placement =
+  | 'right'
+  | 'bottom-end'
+  | 'bottom-start'
+  | 'bottom'
+  | 'left-end'
+  | 'left-start'
+  | 'left'
+  | 'right-end'
+  | 'right-start'
+  | 'top-end'
+  | 'top-start'
+  | 'top';

+ 162 - 0
client/src/components/filter/Filter.tsx

@@ -0,0 +1,162 @@
+import React, { FC, useState, useEffect } from 'react';
+import CustomButton from '../customButton/CustomButton';
+import ICONS from '../icons/Icons';
+import {
+  Typography,
+  makeStyles,
+  Theme,
+  createStyles,
+  Button,
+} from '@material-ui/core';
+import { FilterType } from './Types';
+import Icons from '../icons/Icons';
+import ClickAwayListener from '@material-ui/core/ClickAwayListener';
+import { useTranslation } from 'react-i18next';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      position: 'relative',
+      display: 'inline-block',
+      margin: theme.spacing(0, 2),
+    },
+    count: {
+      display: 'flex',
+      justifyContent: 'center',
+      alignItems: 'center',
+      width: '16px',
+      height: '16px',
+      marginLeft: theme.spacing(1),
+      fontSize: '12px',
+      borderRadius: '50%',
+      backgroundColor: theme.palette.common.white,
+    },
+    options: {
+      position: 'absolute',
+      top: '120%',
+      width: '360px',
+      padding: theme.spacing(4, 3),
+      backgroundColor: theme.palette.common.white,
+      color: 'rgba(0, 0, 0, 0.33)',
+      border: '2px solid rgba(0, 0, 0, 0.15)',
+      zIndex: theme.zIndex.tooltip,
+    },
+    title: {
+      marginBottom: theme.spacing(1),
+      textTransform: 'uppercase',
+      boxShadow: 'initial',
+    },
+    btnRoot: {
+      color: theme.palette.common.black,
+      marginRight: theme.spacing(3),
+      opacity: 0.33,
+      '&:hover': {
+        color: theme.palette.common.black,
+        opacity: 0.6,
+      },
+    },
+    active: {
+      color: theme.palette.common.black,
+      opacity: 0.6,
+      backgroundColor: theme.palette.zilliz.light,
+    },
+    typoButton: {
+      textTransform: 'none',
+    },
+  })
+);
+
+const Filter: FC<FilterType> = props => {
+  const classes = useStyles();
+  const [selected, setSelected] = useState<string[]>([]);
+  const [show, setShow] = useState<boolean>(false);
+  const { t } = useTranslation('btn');
+
+  const { filterOptions = [], onFilter, filterTitle = '' } = props;
+
+  const handleClick = (e: React.MouseEvent) => {
+    setShow(!show);
+  };
+
+  const handleFilter = (value: string) => {
+    !selected.includes(value) && setSelected([...selected, value]);
+  };
+
+  const handleClose = (e: React.MouseEvent, value: string) => {
+    e.stopPropagation();
+    setSelected(v => v.filter(text => value !== text));
+  };
+
+  const CloseIcon = (props: { value: string }) => (
+    <>
+      {Icons.clear({
+        onClick: (e: React.MouseEvent) => handleClose(e, props.value),
+      })}
+    </>
+  );
+
+  useEffect(() => {
+    onFilter && onFilter(selected);
+  }, [selected, onFilter]);
+
+  const handleClickAway = () => {
+    setShow(false);
+  };
+
+  return (
+    <div className={classes.root}>
+      <CustomButton
+        size="small"
+        onClick={handleClick}
+        startIcon={ICONS.filter()}
+        color="primary"
+      >
+        <Typography variant="button">{t('filter')} </Typography>
+        {selected.length ? (
+          <Typography
+            className={classes.count}
+            variant="button"
+            component="span"
+          >
+            {selected.length}
+          </Typography>
+        ) : null}
+      </CustomButton>
+      {show && (
+        <ClickAwayListener onClickAway={handleClickAway}>
+          <div className={classes.options}>
+            <Typography className={classes.title}>{filterTitle}</Typography>
+            {filterOptions.map(v => (
+              <Button
+                key={v.value}
+                size="small"
+                color="default"
+                classes={{
+                  root: classes.btnRoot,
+                }}
+                className={selected.includes(v.value) ? classes.active : ''}
+                endIcon={
+                  selected.includes(v.value) ? (
+                    <CloseIcon value={v.value} />
+                  ) : null
+                }
+                onClick={() => {
+                  handleFilter(v.value);
+                }}
+              >
+                <Typography
+                  variant="button"
+                  classes={{ button: classes.typoButton }}
+                >
+                  {v.label}
+                </Typography>
+              </Button>
+            ))}
+          </div>
+        </ClickAwayListener>
+      )}
+    </div>
+  );
+};
+
+export default Filter;

+ 5 - 0
client/src/components/filter/Types.ts

@@ -0,0 +1,5 @@
+export type FilterType = {
+  filterOptions?: { label: string; value: any }[];
+  onFilter?: (selected: any[]) => void;
+  filterTitle?: string;
+};

+ 57 - 0
client/src/components/grid/ActionBar.tsx

@@ -0,0 +1,57 @@
+import React, { FC } from 'react';
+import { IconButton, makeStyles, Theme, createStyles } from '@material-ui/core';
+import Icons from '../icons/Icons';
+import { ActionBarType } from './Types';
+import CustomToolTip from '../customToolTip/CustomToolTip';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      position: 'relative',
+      display: 'inline-block',
+      marginRight: theme.spacing(1),
+    },
+    tip: {
+      position: 'absolute',
+      left: 0,
+      bottom: '-10px',
+      fontSize: '10px',
+      textTransform: 'capitalize',
+      textAlign: 'center',
+      width: '100%',
+    },
+    disabled: {
+      color: theme.palette.common.black,
+      opacity: 0.15,
+    },
+  })
+);
+
+const ActionBar: FC<ActionBarType> = props => {
+  const classes = useStyles();
+  const { configs, row } = props;
+
+  return (
+    <>
+      {configs.map(v => (
+        <span className={`${classes.root} ${v.className}`} key={v.icon}>
+          <CustomToolTip title={v.label || ''} placement="top">
+            <IconButton
+              aria-label={v.label || ''}
+              onClickCapture={e => {
+                e.stopPropagation();
+                v.onClick(e, row);
+              }}
+              disabled={v.disabled ? v.disabled(row) : false}
+              classes={{ disabled: classes.disabled }}
+            >
+              {Icons[v.icon]()}
+            </IconButton>
+          </CustomToolTip>
+        </span>
+      ))}
+    </>
+  );
+};
+
+export default ActionBar;

+ 41 - 0
client/src/components/grid/LoadingTable.tsx

@@ -0,0 +1,41 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { Skeleton } from '@material-ui/lab';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    padding: theme.spacing(2),
+    paddingTop: 0,
+    backgroundColor: '#fff',
+    borderRadius: '4px',
+  },
+  skeleton: {
+    transform: 'scale(1)',
+    background: 'linear-gradient(90deg, #f0f4f9 0%, #f9f9f9 50%)',
+    borderRadius: '2px',
+  },
+  tr: {
+    display: 'grid',
+    gridTemplateColumns: '10% 89%',
+    gap: '1%',
+    marginTop: theme.spacing(3),
+  },
+}));
+
+const LoadingTable = (props: { wrapperClass?: string; count: number }) => {
+  const { wrapperClass = '', count } = props;
+  const classes = getStyles();
+  const rows = Array(count).fill(1);
+
+  return (
+    <div className={`${classes.wrapper} ${wrapperClass}`}>
+      {rows.map((row, index) => (
+        <div key={index} className={classes.tr}>
+          <Skeleton height={16} classes={{ root: classes.skeleton }} />
+          <Skeleton height={16} classes={{ root: classes.skeleton }} />
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default LoadingTable;

+ 291 - 0
client/src/components/grid/Table.tsx

@@ -0,0 +1,291 @@
+import React, { FC, useEffect, useRef, useState } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableCell from '@material-ui/core/TableCell';
+import TableContainer from '@material-ui/core/TableContainer';
+import TableRow from '@material-ui/core/TableRow';
+import Checkbox from '@material-ui/core/Checkbox';
+import { TableType } from './Types';
+import { Box, Button, Typography } from '@material-ui/core';
+import EnhancedTableHead from './TableHead';
+import { stableSort, getComparator } from './Utils';
+import Copy from '../../components/copy/Copy';
+import ActionBar from './ActionBar';
+import LoadingTable from './LoadingTable';
+
+const useStyles = makeStyles(theme => ({
+  root: {
+    // minHeight: '29vh',
+    width: '100%',
+    flexGrow: 1,
+    flexBasis: 0,
+
+    // change scrollbar style
+    '&::-webkit-scrollbar': {
+      width: '8px',
+    },
+
+    '&::-webkit-scrollbar-track': {
+      backgroundColor: '#f9f9f9',
+    },
+
+    '&::-webkit-scrollbar-thumb': {
+      borderRadius: '8px',
+      backgroundColor: '#eee',
+    },
+  },
+  box: {
+    backgroundColor: '#fff',
+  },
+  paper: {
+    width: '100%',
+    marginBottom: theme.spacing(2),
+  },
+  table: {
+    minWidth: 750,
+  },
+  tableCell: {
+    background: theme.palette.common.white,
+    paddingLeft: theme.spacing(2),
+  },
+  checkbox: {
+    background: theme.palette.common.white,
+  },
+  rowHover: {
+    '&:hover': {
+      // backgroundColor: `${theme.palette.zilliz.light} !important`,
+      backgroundColor: `#f3fcfe`,
+      '& td': {
+        background: 'inherit',
+      },
+    },
+  },
+  cell: {
+    '& p': {
+      display: 'inline-block',
+      verticalAlign: 'middle',
+      overflow: 'hidden',
+      textOverflow: 'ellipsis',
+      whiteSpace: 'nowrap',
+      maxWidth: '300px',
+      fontSize: '12.8px',
+    },
+    '& button': {
+      textTransform: 'inherit',
+      justifyContent: 'flex-start',
+    },
+    // '& svg': {
+    //   color: 'rgba(0, 0, 0, 0.33)',
+    // },
+  },
+  noData: {
+    paddingTop: theme.spacing(6),
+    textAlign: 'center',
+    letterSpacing: '0.5px',
+    color: 'rgba(0, 0, 0, 0.6)',
+  },
+}));
+
+const EnhancedTable: FC<TableType> = props => {
+  const {
+    selected,
+    onSelected,
+    isSelected,
+    onSelectedAll,
+    rows = [],
+    colDefinitions,
+    primaryKey,
+    openCheckBox = true,
+    disableSelect,
+    noData,
+    showHoverStyle,
+    isLoading,
+  } = props;
+  const classes = useStyles();
+  const [order, setOrder] = React.useState('asc');
+  const [orderBy, setOrderBy] = React.useState<string>('');
+  const [tableMouseStatus, setTableMouseStatus] = React.useState<boolean[]>([]);
+  const [loadingRowCount, setLoadingRowCount] = useState<number>(0);
+
+  const containerRef = useRef(null);
+
+  const handleRequestSort = (event: any, property: string) => {
+    const isAsc = orderBy === property && order === 'asc';
+    setOrder(isAsc ? 'desc' : 'asc');
+    setOrderBy(property);
+  };
+
+  useEffect(() => {
+    const height: number = (containerRef.current as any)!.offsetHeight;
+    // table header 57px, loading row 40px
+    const count = Math.floor((height - 57) / 40);
+    setLoadingRowCount(count);
+  }, []);
+
+  return (
+    <TableContainer ref={containerRef} className={classes.root}>
+      <Box height="100%" className={classes.box}>
+        <Table
+          stickyHeader
+          className={classes.table}
+          aria-labelledby="tableTitle"
+          size="medium"
+          aria-label="enhanced table"
+        >
+          <EnhancedTableHead
+            colDefinitions={colDefinitions}
+            numSelected={selected.length}
+            order={order}
+            orderBy={orderBy}
+            onSelectAllClick={onSelectedAll}
+            onRequestSort={handleRequestSort}
+            rowCount={rows.length}
+            openCheckBox={openCheckBox}
+          />
+          {!isLoading && (
+            <TableBody>
+              {rows && rows.length ? (
+                stableSort(rows, getComparator(order, orderBy)).map(
+                  (row, index) => {
+                    const isItemSelected = isSelected(row);
+                    const labelId = `enhanced-table-checkbox-${index}`;
+
+                    const handleMouseEnter = () => {
+                      setTableMouseStatus(v => {
+                        const copy = [...v];
+                        copy[index] = true;
+                        return copy;
+                      });
+                    };
+                    const handleMouseLeave = () =>
+                      setTableMouseStatus(v => {
+                        const copy = [...v];
+                        copy[index] = false;
+                        return copy;
+                      });
+
+                    return (
+                      <TableRow
+                        hover
+                        key={'row' + row[primaryKey] + index}
+                        onClick={event => onSelected(event, row)}
+                        role="checkbox"
+                        aria-checked={isItemSelected}
+                        tabIndex={-1}
+                        selected={isItemSelected && !disableSelect}
+                        classes={
+                          showHoverStyle ? { hover: classes.rowHover } : {}
+                        }
+                        onMouseEnter={handleMouseEnter}
+                        onMouseLeave={handleMouseLeave}
+                      >
+                        {openCheckBox && (
+                          <TableCell
+                            padding="checkbox"
+                            className={classes.checkbox}
+                          >
+                            <Checkbox
+                              checked={isItemSelected}
+                              color="primary"
+                              inputProps={{ 'aria-labelledby': labelId }}
+                            />
+                          </TableCell>
+                        )}
+
+                        {colDefinitions.map((colDef, i) => {
+                          const { actionBarConfigs = [], needCopy = false } =
+                            colDef;
+                          const cellStyle = colDef.getStyle
+                            ? colDef.getStyle(row[colDef.id])
+                            : {};
+                          return colDef.showActionCell ? (
+                            <TableCell
+                              className={`${classes.cell} ${classes.tableCell}`}
+                              key="manage"
+                              style={cellStyle}
+                            >
+                              <ActionBar
+                                showLabel={tableMouseStatus[index]}
+                                configs={actionBarConfigs}
+                                row={row}
+                              ></ActionBar>
+                            </TableCell>
+                          ) : (
+                            <TableCell
+                              key={'cell' + row[primaryKey] + i}
+                              padding={i === 0 ? 'none' : 'default'}
+                              align={colDef.align || 'left'}
+                              className={`${classes.cell} ${classes.tableCell}`}
+                              style={cellStyle}
+                            >
+                              {row[colDef.id] &&
+                              typeof row[colDef.id] === 'string' ? (
+                                <Typography title={row[colDef.id]}>
+                                  {colDef.onClick ? (
+                                    <Button
+                                      color="primary"
+                                      data-data={row[colDef.id]}
+                                      data-index={index}
+                                      onClick={e => {
+                                        colDef.onClick &&
+                                          colDef.onClick(e, row);
+                                      }}
+                                    >
+                                      {row[colDef.id]}
+                                    </Button>
+                                  ) : (
+                                    row[colDef.id]
+                                  )}
+                                </Typography>
+                              ) : (
+                                <>
+                                  {colDef.onClick ? (
+                                    <Button
+                                      color="primary"
+                                      data-data={row[colDef.id]}
+                                      data-index={index}
+                                      onClick={e => {
+                                        colDef.onClick &&
+                                          colDef.onClick(e, row);
+                                      }}
+                                    >
+                                      {row[colDef.id]}
+                                    </Button>
+                                  ) : (
+                                    row[colDef.id]
+                                  )}
+                                </>
+                              )}
+
+                              {needCopy && row[colDef.id] && (
+                                <Copy data={row[colDef.id]} />
+                              )}
+                            </TableCell>
+                          );
+                        })}
+                      </TableRow>
+                    );
+                  }
+                )
+              ) : (
+                <tr>
+                  <td
+                    className={classes.noData}
+                    colSpan={colDefinitions.length}
+                  >
+                    {noData}
+                  </td>
+                </tr>
+              )}
+            </TableBody>
+          )}
+        </Table>
+
+        {isLoading && <LoadingTable count={loadingRowCount} />}
+      </Box>
+    </TableContainer>
+  );
+};
+
+export default EnhancedTable;

+ 109 - 0
client/src/components/grid/TableHead.tsx

@@ -0,0 +1,109 @@
+import React, { FC } from 'react';
+import { TableHeadType } from './Types';
+import {
+  TableHead,
+  TableRow,
+  TableCell,
+  Checkbox,
+  TableSortLabel,
+  makeStyles,
+  Typography,
+} from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+  visuallyHidden: {
+    border: 0,
+    clip: 'rect(0 0 0 0)',
+    height: 1,
+    margin: -1,
+    overflow: 'hidden',
+    padding: 0,
+    position: 'absolute',
+    top: 20,
+    width: 1,
+  },
+  tableCell: {
+    // background: theme.palette.common.t,
+    paddingLeft: theme.spacing(2),
+    // borderBottom: 'none',
+  },
+  tableHeader: {
+    textTransform: 'capitalize',
+    color: 'rgba(0, 0, 0, 0.6)',
+    fontSize: '12.8px',
+  },
+  tableRow: {
+    // borderBottom: '1px solid rgba(0, 0, 0, 0.6);',
+  },
+}));
+
+const EnhancedTableHead: FC<TableHeadType> = props => {
+  const {
+    onSelectAllClick,
+    order,
+    orderBy,
+    numSelected,
+    rowCount,
+    colDefinitions = [],
+    onRequestSort,
+    openCheckBox,
+  } = props;
+  const classes = useStyles();
+  const createSortHandler = (property: string) => (event: React.MouseEvent) => {
+    onRequestSort(event, property);
+  };
+
+  return (
+    <TableHead>
+      <TableRow className={classes.tableRow}>
+        {openCheckBox && (
+          <TableCell padding="checkbox">
+            <Checkbox
+              color="primary"
+              indeterminate={numSelected > 0 && numSelected < rowCount}
+              checked={rowCount > 0 && numSelected === rowCount}
+              onChange={onSelectAllClick}
+              inputProps={{ 'aria-label': 'select all desserts' }}
+            />
+          </TableCell>
+        )}
+
+        {colDefinitions.map(headCell => (
+          <TableCell
+            key={headCell.id}
+            align={headCell.align || 'left'}
+            padding={headCell.disablePadding ? 'none' : 'default'}
+            sortDirection={orderBy === headCell.id ? order : false}
+            className={classes.tableCell}
+          >
+            {headCell.label ? (
+              <TableSortLabel
+                active={orderBy === headCell.id}
+                direction={orderBy === headCell.id ? order : 'asc'}
+                onClick={createSortHandler(headCell.id)}
+              >
+                <Typography variant="body1" className={classes.tableHeader}>
+                  {headCell.label}
+                </Typography>
+
+                {orderBy === headCell.id ? (
+                  <Typography className={classes.visuallyHidden}>
+                    {order === 'desc'
+                      ? 'sorted descending'
+                      : 'sorted ascending'}
+                  </Typography>
+                ) : null}
+              </TableSortLabel>
+            ) : (
+              <Typography variant="body1" className={classes.tableHeader}>
+                {headCell.label}
+              </Typography>
+            )}
+          </TableCell>
+        ))}
+      </TableRow>
+    </TableHead>
+  );
+};
+
+export default EnhancedTableHead;

+ 83 - 0
client/src/components/grid/TablePaginationActions.tsx

@@ -0,0 +1,83 @@
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  IconButton,
+  Typography,
+} from '@material-ui/core';
+import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { TablePaginationActionsProps } from './Types';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      display: 'flex',
+      alignItems: 'center',
+      flexShrink: 0,
+      marginLeft: theme.spacing(2.5),
+    },
+    page: {
+      display: 'flex',
+      justifyContent: 'center',
+      alignItems: 'center',
+      width: '24px',
+      height: '24px',
+      backgroundColor: theme.palette.common.white,
+    },
+    btn: {
+      width: '24px',
+      height: '24px',
+      border: '1px solid #c4c4c4',
+      borderRadius: '2px 0 0 2px',
+      backgroundColor: 'rgba(0,0,0,0.1)',
+      cursor: 'pointer',
+    },
+  })
+);
+
+const TablePaginationActions = (props: TablePaginationActionsProps) => {
+  const classes = useStyles();
+  const { count, page, rowsPerPage, onChangePage } = props;
+  const { t } = useTranslation();
+  const gridTrans = t('grid') as any;
+
+  const handleBackButtonClick = (
+    event: React.MouseEvent<HTMLButtonElement>
+  ) => {
+    onChangePage(event, page - 1);
+  };
+
+  const handleNextButtonClick = (
+    event: React.MouseEvent<HTMLButtonElement>
+  ) => {
+    onChangePage(event, page + 1);
+  };
+
+  return (
+    <div className={classes.root}>
+      <IconButton
+        onClick={handleBackButtonClick}
+        disabled={page === 0}
+        aria-label={gridTrans.prevLabel}
+        className={classes.btn}
+      >
+        <KeyboardArrowLeft />
+      </IconButton>
+      <Typography variant="body2" className={classes.page}>
+        {page + 1}
+      </Typography>
+      <IconButton
+        onClick={handleNextButtonClick}
+        disabled={page >= Math.ceil(count / rowsPerPage) - 1}
+        aria-label={gridTrans.nextLabel}
+        className={classes.btn}
+      >
+        <KeyboardArrowRight />
+      </IconButton>
+    </div>
+  );
+};
+
+export default TablePaginationActions;

+ 59 - 0
client/src/components/grid/TableSwitch.tsx

@@ -0,0 +1,59 @@
+import { makeStyles, Theme, createStyles } from '@material-ui/core';
+import React, { FC, useState } from 'react';
+import Icons from '../icons/Icons';
+import { TableSwitchType } from './Types';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      display: 'flex',
+    },
+    line: {
+      display: 'inline-block',
+      margin: theme.spacing(0, 1),
+      border: '1px solid rgba(0, 0, 0, 0.15)',
+    },
+    btn: {
+      cursor: 'pointer',
+      color: 'rgba(0, 0, 0, 0.15)',
+    },
+    active: {
+      color: 'rgba(0, 0, 0, 0.6) ',
+    },
+  })
+);
+
+const TableSwitch: FC<TableSwitchType> = props => {
+  const { defaultActive = 'list', onListClick, onAppClick } = props;
+  const [active, setActive] = useState(defaultActive);
+  const classes = useStyles();
+  const IconList = Icons.list;
+  const IconApp = Icons.app;
+
+  const handleListClick = () => {
+    setActive('list');
+    onListClick();
+  };
+
+  const handleAppClick = () => {
+    setActive('app');
+    onAppClick();
+  };
+
+  return (
+    <div className={classes.root}>
+      <IconList
+        className={`${classes.btn} ${active === 'list' ? classes.active : ''}`}
+        role="button"
+        onClick={handleListClick}
+      />
+      <span className={classes.line}></span>
+      <IconApp
+        className={`${classes.btn} ${active === 'app' ? classes.active : ''}`}
+        onClick={handleAppClick}
+      />
+    </div>
+  );
+};
+
+export default TableSwitch;

+ 175 - 0
client/src/components/grid/ToolBar.tsx

@@ -0,0 +1,175 @@
+import React, { FC, useMemo } from 'react';
+import {
+  Grid,
+  Typography,
+  makeStyles,
+  Theme,
+  createStyles,
+  IconButton,
+} from '@material-ui/core';
+import CustomButton from '../customButton/CustomButton';
+import Icons from '../icons/Icons';
+import { ToolBarConfig, ToolBarType } from './Types';
+import Filter from '../filter/Filter';
+import SearchInput from '../textField/SearchInput';
+import TableSwitch from './TableSwitch';
+import { throwErrorForDev } from '../../utils/Common';
+import CustomIconButton from '../customButton/CustomIconButton';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    countLabel: {
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'flex-end',
+      color: theme.palette.common.black,
+      opacity: 0.4,
+    },
+    btn: {
+      // marginLeft: theme.spacing(1),
+      marginRight: '12px',
+    },
+    gridEnd: {
+      display: 'flex',
+      justifyContent: 'flex-end',
+      alignItems: 'center',
+    },
+  })
+);
+
+const CustomToolBar: FC<ToolBarType> = props => {
+  const {
+    toolbarConfigs,
+    filterOptions = [],
+    onFilter,
+    filterTitle,
+    selected = [],
+  } = props;
+  const classes = useStyles();
+
+  // remove hidden button
+  const leftConfigs = useMemo(() => {
+    return toolbarConfigs.filter(
+      (c: ToolBarConfig) =>
+        !c.hidden &&
+        c.icon !== 'search' &&
+        c.type !== 'switch' &&
+        c.position !== 'right'
+    );
+  }, [toolbarConfigs]);
+
+  const rightConfigs = useMemo(() => {
+    return toolbarConfigs.filter(
+      c => c.icon === 'search' || c.type === 'switch' || c.position === 'right'
+    );
+  }, [toolbarConfigs]);
+
+  return (
+    <>
+      <Grid container spacing={2}>
+        <Grid item xs={8}>
+          {leftConfigs.map((c, i) => {
+            const isSelect = c.type === 'select' || c.type === 'groupSelect';
+            if (isSelect) {
+              return c.component;
+            }
+
+            const Icon = c.icon ? Icons[c.icon!]() : '';
+            const disabled = c.disabled ? c.disabled(selected) : false;
+            const isIcon = c.type === 'iconBtn';
+
+            const btn = (
+              <CustomButton
+                key={i}
+                size="small"
+                onClick={c.onClick}
+                startIcon={Icon}
+                color="primary"
+                disabled={disabled}
+                variant="contained"
+                tooltip={c.tooltip}
+                className={classes.btn}
+              >
+                <Typography variant="button">{c.label}</Typography>
+              </CustomButton>
+            );
+
+            const iconBtn = (
+              <CustomIconButton
+                key={i}
+                onClick={c.onClick}
+                tooltip={c.tooltip}
+                disabled={disabled}
+              >
+                {Icon}
+              </CustomIconButton>
+            );
+
+            return isIcon ? iconBtn : btn;
+          })}
+          {filterOptions.length && onFilter ? (
+            <Filter
+              filterOptions={filterOptions}
+              onFilter={onFilter}
+              filterTitle={filterTitle}
+            ></Filter>
+          ) : null}
+        </Grid>
+
+        {rightConfigs.length > 0 && (
+          <Grid className={classes.gridEnd} item xs={4}>
+            {rightConfigs.map((c, i) => {
+              if (c.icon === 'search') {
+                if (!c.onSearch) {
+                  return throwErrorForDev(
+                    `if icon is search  onSearch event handler is required`
+                  );
+                }
+                return (
+                  <SearchInput
+                    onClear={c.onClear}
+                    onSearch={c.onSearch}
+                    searchText={c.searchText}
+                    key={i}
+                  />
+                );
+              }
+              switch (c.type) {
+                case 'switch':
+                  if (!c.onAppClick || !c.onListClick) {
+                    return throwErrorForDev(
+                      `if type is switch need onAppClick onListClick event handler`
+                    );
+                  }
+                  return (
+                    <TableSwitch
+                      onAppClick={c.onAppClick}
+                      onListClick={c.onListClick}
+                      key={i}
+                    />
+                  );
+                case 'select':
+                case 'groupSelect':
+                  if (!c.component) {
+                    return throwErrorForDev(`component prop is required`);
+                  }
+                  return c.component;
+                default:
+                  const Icon = c.icon ? Icons[c.icon]() : '';
+                  return Icon ? (
+                    <IconButton onClick={c.onClick} key={i}>
+                      {Icon}
+                    </IconButton>
+                  ) : (
+                    <div key={i}>Need Icon</div>
+                  );
+              }
+            })}
+          </Grid>
+        )}
+      </Grid>
+    </>
+  );
+};
+
+export default CustomToolBar;

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

@@ -0,0 +1,135 @@
+import { IconsType } from '../icons/Types';
+import { FilterType } from '../filter/Types';
+import { SearchType } from '../textField/Types';
+import { ReactElement } from 'react';
+
+export type IconConfigType = {
+  [x: string]: JSX.Element;
+};
+
+export type ColorType = 'default' | 'inherit' | 'primary' | 'secondary';
+
+/**
+ * selected: selected data in table checkbox
+ */
+export type ToolBarType = FilterType & {
+  toolbarConfigs: ToolBarConfig[];
+  selected?: any[];
+  setSelected?: (selected: any[]) => void;
+};
+
+export type TableSwitchType = {
+  defaultActive?: 'list' | 'app';
+  onListClick: () => void;
+  onAppClick: () => void;
+};
+
+/**
+ * postion: toolbar position
+ * component: when type is not iconBtn button switch, render component
+ */
+export type ToolBarConfig = Partial<TableSwitchType> &
+  Partial<SearchType> & {
+    label: string;
+    icon?: IconsType;
+    color?: ColorType;
+    // when type is not iconBtn, onClick is optional
+    onClick?: (arg0: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
+    disabled?: (data: any[]) => boolean;
+    tooltip?: string;
+    hidden?: boolean;
+    type?: 'iconBtn' | 'buttton' | 'switch' | 'select' | 'groupSelect';
+    position?: 'right' | 'left';
+    component?: ReactElement;
+  };
+
+export type TableHeadType = {
+  onSelectAllClick: (e: React.ChangeEvent) => void;
+  order: any;
+  orderBy: string;
+  numSelected: number;
+  rowCount: number;
+  colDefinitions: ColDefinitionsType[];
+  onRequestSort: (e: any, p: string) => void;
+  openCheckBox?: boolean;
+};
+
+export type TableType = {
+  selected: any[];
+  onSelected: (e: React.MouseEvent, row: any) => void;
+  isSelected: (data: any[]) => boolean;
+  onSelectedAll: (e: React.ChangeEvent) => void;
+  rows?: any[];
+  colDefinitions: ColDefinitionsType[];
+  primaryKey: string;
+  openCheckBox?: boolean;
+  disableSelect?: boolean;
+  noData?: string;
+  showHoverStyle?: boolean;
+  isLoading?: boolean;
+};
+
+export type ColDefinitionsType = {
+  id: string;
+  align?: 'inherit' | 'left' | 'center' | 'right' | 'justify' | undefined;
+  disablePadding: boolean;
+  label: React.ReactNode;
+  needCopy?: boolean;
+  showActionCell?: boolean;
+  onClick?: (
+    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
+    data?: any
+  ) => void;
+  getStyle?: (data: any) => {};
+
+  onConnect?: (
+    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
+    data: any
+  ) => void;
+  actionBarConfigs?: ActionBarConfig[];
+};
+
+export type MilvusGridType = ToolBarType & {
+  rowCount: number;
+  rowsPerPage?: number;
+  primaryKey: string;
+  onChangePage?: (e: any, nextPageNum: number) => void;
+  labelDisplayedRows?: (obj: any) => string;
+  page?: number;
+  showToolbar?: boolean;
+  rows: any[];
+  colDefinitions: ColDefinitionsType[];
+  isLoading?: boolean;
+  title?: string[];
+  searchForm?: React.ReactNode;
+  openCheckBox?: boolean;
+  titleIcon?: React.ReactNode;
+  pageUnit?: string;
+  disableSelect?: boolean;
+  noData?: string;
+  showHoverStyle?: boolean;
+};
+
+export type ActionBarType = {
+  configs: ActionBarConfig[];
+  row: any;
+  showLabel?: boolean;
+};
+
+type ActionBarConfig = {
+  onClick: (e: React.MouseEvent, row: any) => void;
+  icon: IconsType;
+  label?: string;
+  className?: string;
+  disabled?: (row: any) => boolean;
+};
+
+export type TablePaginationActionsProps = {
+  count: number;
+  page: number;
+  rowsPerPage: number;
+  onChangePage: (
+    event: React.MouseEvent<HTMLButtonElement>,
+    newPage: number
+  ) => void;
+};

+ 36 - 0
client/src/components/grid/Utils.ts

@@ -0,0 +1,36 @@
+type numberObj = {
+  [x: string]: number;
+};
+
+export const descendingComparator = (
+  a: numberObj,
+  b: numberObj,
+  orderBy: React.ReactText
+) => {
+  if (b[orderBy] < a[orderBy]) {
+    return -1;
+  }
+  if (b[orderBy] > a[orderBy]) {
+    return 1;
+  }
+  return 0;
+};
+
+export const getComparator = (order: string, orderBy: string) => {
+  return order === 'desc'
+    ? (a: numberObj, b: numberObj) => descendingComparator(a, b, orderBy)
+    : (a: numberObj, b: numberObj) => -descendingComparator(a, b, orderBy);
+};
+
+export const stableSort = (
+  array: any[],
+  comparator: { (a: numberObj, b: numberObj): number }
+) => {
+  const stabilizedThis = array.map((el, index) => [el, index]);
+  stabilizedThis.sort((a, b) => {
+    const order = comparator(a[0], b[0]);
+    if (order !== 0) return order;
+    return a[1] - b[1];
+  });
+  return stabilizedThis.map(el => el[0]);
+};

+ 234 - 0
client/src/components/grid/index.tsx

@@ -0,0 +1,234 @@
+import React, { FC, MouseEvent } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Grid from '@material-ui/core/Grid';
+import Breadcrumbs from '@material-ui/core/Breadcrumbs';
+import TablePagination from '@material-ui/core/TablePagination';
+import Typography from '@material-ui/core/Typography';
+import CustomToolbar from './ToolBar';
+import Table from './Table';
+import { MilvusGridType } from './Types';
+import { useTranslation } from 'react-i18next';
+import TablePaginationActions from './TablePaginationActions';
+
+const userStyle = makeStyles(theme => ({
+  loading: {
+    height: '100%',
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: theme.spacing(20),
+    width: '100%',
+  },
+  titleIcon: {
+    verticalAlign: '-3px',
+    '& svg': {
+      fill: '#32363c',
+    },
+  },
+  tableTitle: {
+    '& .last': {
+      color: 'rgba(0, 0, 0, 0.54)',
+    },
+  },
+  noData: {
+    pointerEvents: 'none',
+    color: '#999',
+    textAlign: 'center',
+    height: '50vh',
+    display: 'grid',
+    justifyContent: 'center',
+    alignContent: 'center',
+    fontSize: '32px',
+  },
+  pagenation: {
+    '& .MuiTablePagination-caption': {
+      position: 'absolute',
+      left: 0,
+      bottom: 0,
+      top: 0,
+      display: 'flex',
+      alignItems: 'center',
+      '& .rows': {
+        color: 'rgba(0,0,0,0.33)',
+        marginLeft: theme.spacing(1),
+      },
+    },
+  },
+
+  noBottomPadding: {
+    paddingBottom: '0 !important',
+    display: 'flex',
+    flexDirection: 'column',
+  },
+
+  wrapper: {
+    height: '100%',
+  },
+  container: {
+    flexWrap: 'nowrap',
+    flexDirection: 'column',
+  },
+}));
+
+const MilvusGrid: FC<MilvusGridType> = props => {
+  const classes = userStyle();
+  const { t } = useTranslation();
+  const gridTrans = t('grid') as any;
+  const {
+    rowCount = 10,
+    rowsPerPage = 5,
+    primaryKey = 'id',
+    showToolbar = false,
+    toolbarConfigs = [],
+    onChangePage = (
+      e: MouseEvent<HTMLButtonElement> | null,
+      nextPageNum: number
+    ) => {
+      console.log('nextPageNum', nextPageNum);
+    },
+    labelDisplayedRows,
+    // pageUnit = 'item',
+    page = 0,
+    rows = [],
+    colDefinitions = [],
+    isLoading = false,
+    title,
+    // titleIcon = <CollectionIcon />,
+    searchForm,
+    openCheckBox = true,
+    disableSelect = false,
+    noData = t('grid.noData'),
+    filterOptions = [],
+    onFilter,
+    filterTitle,
+    showHoverStyle = true,
+    selected = [],
+    setSelected = () => {},
+  } = props;
+
+  const _isSelected = (row: { [x: string]: any }) => {
+    // console.log("row selected test", row[primaryKey]);
+    return selected.some((s: any) => s[primaryKey] === row[primaryKey]);
+  };
+
+  const _onSelected = (event: React.MouseEvent, row: { [x: string]: any }) => {
+    let newSelected: any[] = ([] as any[]).concat(selected);
+    if (_isSelected(row)) {
+      newSelected = newSelected.filter(s => s[primaryKey] !== row[primaryKey]);
+    } else {
+      newSelected.push(row);
+    }
+
+    setSelected(newSelected);
+  };
+
+  const _onSelectedAll = (event: React.ChangeEvent) => {
+    if ((event.target as HTMLInputElement).checked) {
+      const newSelecteds = rows;
+      setSelected(newSelecteds);
+      return;
+    }
+    setSelected([]);
+  };
+
+  // const defaultLabelRows = ({ from = 0, to = 0, count = 0 }) => {
+  //   const plural = pageUnit.charAt(pageUnit.length - 1) === 'y' ? 'ies' : 's';
+  //   const formatUnit =
+  //     pageUnit.charAt(pageUnit.length - 1) === 'y'
+  //       ? pageUnit.slice(0, pageUnit.length - 1)
+  //       : pageUnit;
+  //   const unit = count > 1 ? `${formatUnit}${plural}` : pageUnit;
+  //   return `${count} ${unit}`;
+  // };
+
+  const defaultLabelRows = ({ from = 0, to = 0, count = 0 }) => {
+    return (
+      <>
+        <Typography variant="body2" component="span">
+          {from} - {to}
+        </Typography>
+        <Typography variant="body2" className="rows" component="span">
+          {gridTrans.of} {count} {gridTrans.rows}
+        </Typography>
+      </>
+    );
+  };
+  return (
+    <Grid
+      container
+      classes={{ root: classes.wrapper, container: classes.container }}
+      spacing={3}
+    >
+      {title && (
+        <Grid item xs={12} className={classes.tableTitle}>
+          <Breadcrumbs separator="›" aria-label="breadcrumb">
+            {title.map(
+              (v: any, i: number) =>
+                v && (
+                  <Typography
+                    key={v}
+                    className={i === title.length - 1 ? 'last' : ''}
+                    variant="h6"
+                    color="textPrimary"
+                  >
+                    {v}
+                  </Typography>
+                )
+            )}
+          </Breadcrumbs>
+        </Grid>
+      )}
+
+      {searchForm && (
+        <Grid item xs={12}>
+          {searchForm}
+        </Grid>
+      )}
+
+      {(showToolbar || toolbarConfigs.length > 0) && (
+        <Grid item>
+          <CustomToolbar
+            toolbarConfigs={toolbarConfigs}
+            filterOptions={filterOptions}
+            onFilter={onFilter}
+            filterTitle={filterTitle}
+            selected={selected}
+          ></CustomToolbar>
+        </Grid>
+      )}
+
+      <Grid item xs={12} className={classes.noBottomPadding}>
+        <Table
+          openCheckBox={openCheckBox}
+          primaryKey={primaryKey}
+          rows={rows}
+          selected={selected}
+          colDefinitions={colDefinitions}
+          onSelectedAll={_onSelectedAll}
+          onSelected={_onSelected}
+          isSelected={_isSelected}
+          disableSelect={disableSelect}
+          noData={noData}
+          showHoverStyle={showHoverStyle}
+          isLoading={isLoading}
+        ></Table>
+        {rowCount ? (
+          <TablePagination
+            component="div"
+            colSpan={3}
+            count={rowCount}
+            page={page}
+            labelDisplayedRows={labelDisplayedRows || defaultLabelRows}
+            rowsPerPage={rowsPerPage}
+            rowsPerPageOptions={[]}
+            onChangePage={onChangePage}
+            className={classes.pagenation}
+            ActionsComponent={TablePaginationActions}
+          />
+        ) : null}
+      </Grid>
+    </Grid>
+  );
+};
+
+export default MilvusGrid;

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

@@ -0,0 +1,33 @@
+import React from 'react';
+import { IconsType } from './Types';
+import SearchIcon from '@material-ui/icons/Search';
+import AddIcon from '@material-ui/icons/Add';
+import DeleteIcon from '@material-ui/icons/Delete';
+import FileCopyIcon from '@material-ui/icons/FileCopy';
+import Visibility from '@material-ui/icons/Visibility';
+import VisibilityOff from '@material-ui/icons/VisibilityOff';
+import ClearIcon from '@material-ui/icons/Clear';
+import FilterListIcon from '@material-ui/icons/FilterList';
+import ReorderIcon from '@material-ui/icons/Reorder';
+import AppsIcon from '@material-ui/icons/Apps';
+import MoreVertIcon from '@material-ui/icons/MoreVert';
+import CancelIcon from '@material-ui/icons/Cancel';
+import CheckCircleIcon from '@material-ui/icons/CheckCircle';
+
+const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
+  search: (props = {}) => <SearchIcon {...props} />,
+  add: (props = {}) => <AddIcon {...props} />,
+  delete: (props = {}) => <DeleteIcon {...props} />,
+  list: (props = {}) => <ReorderIcon {...props} />,
+  copy: (props = {}) => <FileCopyIcon {...props} />,
+  visible: (props = {}) => <Visibility {...props} />,
+  invisible: (props = {}) => <VisibilityOff {...props} />,
+  error: (props = {}) => <CancelIcon {...props} />,
+  clear: (props = {}) => <ClearIcon {...props} />,
+  filter: (props = {}) => <FilterListIcon {...props} />,
+  more: (props = {}) => <MoreVertIcon {...props} />,
+  app: (props = {}) => <AppsIcon {...props} />,
+  success: (props = {}) => <CheckCircleIcon {...props} />,
+};
+
+export default icons;

+ 14 - 0
client/src/components/icons/Types.ts

@@ -0,0 +1,14 @@
+export type IconsType =
+  | 'search'
+  | 'add'
+  | 'delete'
+  | 'list'
+  | 'copy'
+  | 'visible'
+  | 'invisible'
+  | 'error'
+  | 'clear'
+  | 'filter'
+  | 'app'
+  | 'more'
+  | 'success';

+ 44 - 0
client/src/components/layout/GlobalEffect.tsx

@@ -0,0 +1,44 @@
+import React, { useContext } from 'react';
+import axiosInstance from '../../http/Axios';
+import { rootContext } from '../../context/Root';
+import { CODE_STATUS } from '../../consts/Http';
+
+let axiosResInterceptor: number | null = null;
+// let timer: Record<string, ReturnType<typeof setTimeout> | number>[] = [];
+// we only take side effect here, nothing else
+const GlobalEffect = (props: { children: React.ReactNode }) => {
+  const { openSnackBar } = useContext(rootContext);
+
+  // catch axios error here
+  if (axiosResInterceptor === null) {
+    axiosResInterceptor = axiosInstance.interceptors.response.use(
+      function (res: any) {
+        if (res.data && res.data.code !== CODE_STATUS.SUCCESS) {
+          openSnackBar(res.data.message, 'warning');
+          return Promise.reject(res.data);
+        }
+
+        return res;
+      },
+      function (error: any) {
+        const { response = {} } = error;
+
+        if (response.data) {
+          const { message: errMsg } = response.data;
+
+          errMsg && openSnackBar(errMsg, 'error');
+          return Promise.reject(error);
+        }
+        if (error.message) {
+          openSnackBar(error.message, 'error');
+        }
+        return Promise.reject(error);
+      }
+    );
+  }
+  // get global data
+
+  return <>{props.children}</>;
+};
+
+export default GlobalEffect;

+ 105 - 0
client/src/components/layout/GlobalToolbar.tsx

@@ -0,0 +1,105 @@
+import React, { useContext } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  // Divider
+} from '@material-ui/core';
+import icons from '../icons/Icons';
+// import CustomButton from '../customButton/CustomButton';
+import SimpleMenu from '../menu/SimpleMenu';
+import { useTranslation } from 'react-i18next';
+import { GlobalCreateType } from './Types';
+import { StatusEnum } from '../status/Types';
+// import { rootContext } from '../../context/Root';
+// import GlobalSearch from './GlobalSearch';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      display: 'flex',
+      background: theme.palette.common.white,
+      marginBottom: theme.spacing(3),
+      marginTop: theme.spacing(3),
+      backgroundColor: 'transparent',
+    },
+    buttonWrapper: {
+      boxSizing: 'border-box',
+      padding: theme.spacing(1, 2.5),
+      backgroundColor: theme.palette.common.white,
+      width: (props: any) => props.width,
+
+      display: 'flex',
+      flexDirection: 'column',
+    },
+    button: {
+      paddingLeft: theme.spacing(1),
+      color: theme.palette.common.black,
+      marginTop: theme.spacing(1),
+    },
+    btn: {
+      width: '100%',
+      padding: theme.spacing(1, 0),
+      fontSize: '16px',
+      lineHeight: '24px',
+    },
+    breadcrumb: {
+      padding: theme.spacing(1, 2),
+      backgroundColor: theme.palette.common.white,
+      marginLeft: '2px',
+      flex: 1,
+    },
+    divider: {
+      margin: theme.spacing(1, 0),
+    },
+    container: {},
+  })
+);
+
+const GlobalToolbar = (props: { width: String }) => {
+  const { t } = useTranslation();
+  const classes = useStyles(props);
+  const { t: btnTrans } = useTranslation('btn');
+  const navTrans: any = t('nav');
+
+  // const SearchIcon = icons.search;
+  const AddIcon = icons.add;
+
+  // const handleGlobalSearch = () => {
+  //   openDialog({
+  //     open: true,
+  //     type: 'custom',
+  //     params: {
+  //       component: <GlobalSearch options={top100Films} />,
+  //       containerClass: classes.container,
+  //     },
+  //   });
+  // };
+
+  return (
+    <div className={classes.root}>
+      <div className={classes.buttonWrapper}>
+        <SimpleMenu
+          label={btnTrans('create')}
+          menuItems={[]}
+          buttonProps={{
+            startIcon: <AddIcon />,
+            variant: 'contained',
+            className: classes.btn,
+          }}
+        ></SimpleMenu>
+
+        {/* <CustomButton
+          startIcon={<SearchIcon />}
+          variant="outlined"
+          className={classes.button}
+          onClick={handleGlobalSearch}
+        >
+          {btnTrans('search')}
+        </CustomButton> */}
+      </div>
+    </div>
+  );
+};
+
+export default GlobalToolbar;

+ 70 - 0
client/src/components/layout/Header.tsx

@@ -0,0 +1,70 @@
+import React, { FC } from 'react';
+import { makeStyles, Theme, createStyles } from '@material-ui/core';
+import { HeaderType } from './Types';
+// import { useTranslation } from 'react-i18next';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    header: {
+      display: 'flex',
+      alignItems: 'center',
+      color: theme.palette.common.black,
+      marginRight: theme.spacing(5),
+    },
+    contentWrapper: {
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      paddingTop: theme.spacing(3),
+      paddingLeft: theme.spacing(6),
+      flex: 1,
+    },
+    navigation: {
+      display: 'flex',
+      alignItems: 'center',
+      fontWeight: 'bold',
+      '& svg': {
+        fontSize: '16px',
+        cursor: 'pointer',
+      },
+    },
+    changePwdTip: {
+      width: '420px',
+      textAlign: 'center',
+      '& span': {
+        fontStyle: 'italic',
+      },
+    },
+    user: {
+      display: 'flex',
+    },
+    menuLabel: {
+      height: '100%',
+      color: '#010e29',
+      fontSize: '14px',
+      lineHeight: '20px',
+
+      '&:hover': {
+        backgroundColor: 'transparent',
+      },
+    },
+    arrow: {
+      color: theme.palette.primary.main,
+    },
+    icon: {
+      color: theme.palette.primary.main,
+    },
+  })
+);
+
+const Header: FC<HeaderType> = props => {
+  const classes = useStyles();
+
+  return (
+    <header className={classes.header}>
+      <div className={classes.contentWrapper}>header</div>
+    </header>
+  );
+};
+
+export default Header;

+ 60 - 0
client/src/components/layout/Layout.tsx

@@ -0,0 +1,60 @@
+import GlobalEffect from './GlobalEffect';
+import Header from './Header';
+import { makeStyles, Theme, createStyles } from '@material-ui/core';
+import NavMenu from '../menu/NavMenu';
+import { NavMenuItem } from '../menu/Types';
+// import { useHistory } from 'react-router';
+// import { useTranslation } from 'react-i18next';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      minHeight: '100vh',
+      backgroundColor: (props: any) => props.backgroundColor,
+    },
+    content: {
+      display: 'flex',
+    },
+    body: {
+      flex: 1,
+      display: 'flex',
+      flexDirection: 'column',
+      height: `100vh`,
+      overflowY: 'scroll',
+    },
+  })
+);
+
+const Layout = (props: any) => {
+  // const history = useHistory();
+  const path = window.location.hash.slice(2);
+  const greyPaths = ['', 'billing'];
+  const bgColor = greyPaths.includes(path) ? '#f5f5f5' : '#fff';
+  const classes = useStyles({ backgroundColor: bgColor });
+
+  // const { t } = useTranslation();
+
+  const data: NavMenuItem[] = [];
+
+  return (
+    <div className={classes.root}>
+      <GlobalEffect>
+        <div className={classes.content}>
+          <NavMenu
+            width="200px"
+            data={data}
+            defaultActive="Lock"
+            defaultOpen={{ security: true }}
+          />
+
+          <div className={classes.body}>
+            <Header />
+            {props.children}
+          </div>
+        </div>
+      </GlobalEffect>
+    </div>
+  );
+};
+
+export default Layout;

+ 5 - 0
client/src/components/layout/Types.ts

@@ -0,0 +1,5 @@
+export type HeaderType = {
+  onlyLogo?: boolean;
+};
+
+export type GlobalCreateType = 'database' | 'query';

+ 37 - 0
client/src/components/layout/UserContainer.tsx

@@ -0,0 +1,37 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import React, { ReactElement } from 'react';
+import backgroundPath from '../../assets/imgs/background.png';
+
+const getContainerStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    width: '100%',
+    height: '90%',
+    backgroundImage: `url(${backgroundPath})`,
+    backgroundRepeat: 'no-repeat',
+    backgroundSize: 'cover',
+
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  card: {
+    width: '480px',
+    backgroundColor: '#fff',
+    boxShadow: '0px 10px 30px rgba(0, 0, 0, 0.1)',
+    borderRadius: '8px',
+    padding: theme.spacing(5, 0),
+  },
+}));
+
+// used for user login process
+const UserContainer = ({ children }: { children: ReactElement }) => {
+  const classes = getContainerStyles();
+
+  return (
+    <section className={classes.wrapper}>
+      <div className={classes.card}>{children}</div>
+    </section>
+  );
+};
+
+export default UserContainer;

+ 186 - 0
client/src/components/menu/NavMenu.tsx

@@ -0,0 +1,186 @@
+import React, { FC, useEffect } from 'react';
+import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import Collapse from '@material-ui/core/Collapse';
+import ExpandLess from '@material-ui/icons/ExpandLess';
+import ExpandMore from '@material-ui/icons/ExpandMore';
+import { NavMenuItem, NavMenuType } from './Types';
+import logoPath from '../../assets/imgs/logo.png';
+import { useTranslation } from 'react-i18next';
+import { useLocation } from 'react-router';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      width: (props: any) => props.width || '100%',
+      background: theme.palette.common.white,
+
+      paddingBottom: theme.spacing(5),
+      display: 'flex',
+      flexDirection: 'column',
+      justifyContent: 'space-between',
+    },
+    nested: {
+      paddingLeft: theme.spacing(4),
+    },
+    item: {
+      marginBottom: theme.spacing(2),
+      marginLeft: theme.spacing(3),
+
+      width: 'initial',
+      color: '#82838e',
+
+      '& svg': {
+        width: '20px',
+        height: '20px',
+
+        '& path': {
+          stroke: '#82838e',
+        },
+
+        '& .st0': {
+          fill: '#82838e',
+        },
+      },
+    },
+    itemIcon: {
+      minWidth: '20px',
+      marginRight: theme.spacing(1),
+    },
+    active: {
+      color: theme.palette.primary.main,
+      borderRight: `2px solid ${theme.palette.primary.main}`,
+
+      '& svg': {
+        '& path': {
+          stroke: theme.palette.primary.main,
+
+          transition: 'all 0.2s',
+        },
+
+        '& .st0': {
+          fill: theme.palette.primary.main,
+
+          transition: 'all 0.2s',
+        },
+      },
+    },
+
+    logoWrapper: {
+      width: '100%',
+      display: 'flex',
+      justifyContent: 'center',
+      alignItems: 'center',
+
+      marginTop: '30px',
+
+      marginBottom: '65px',
+    },
+    logo: {
+      width: '150px',
+    },
+
+    feedback: {
+      color: theme.palette.primary.main,
+
+      '&:hover': {
+        backgroundColor: '#fff',
+      },
+    },
+  })
+);
+
+const NavMenu: FC<NavMenuType> = props => {
+  const { width, data, defaultActive = '', defaultOpen = {} } = props;
+  const classes = useStyles({ width });
+  const [open, setOpen] = React.useState<{ [x: string]: boolean }>(defaultOpen);
+  const [active, setActive] = React.useState<string>(defaultActive);
+  const location = useLocation();
+
+  const handleClick = (label: string) => {
+    setOpen(v => ({
+      ...v,
+      [label]: !v[label],
+    }));
+  };
+
+  const { t } = useTranslation();
+  const navTrans: { [key in string]: string | object } = t('nav');
+
+  useEffect(() => {
+    const activeLabel = location.pathname.includes('queries')
+      ? (navTrans.query as string)
+      : (navTrans.database as string);
+    setActive(activeLabel);
+  }, [location.pathname, navTrans.query, navTrans.database]);
+
+  const NestList = (props: { data: NavMenuItem[]; className?: string }) => {
+    const { className, data } = props;
+    return (
+      <>
+        {data.map((v: any) => {
+          const IconComponent = v.icon;
+          if (v.children) {
+            return (
+              <div key={v.label}>
+                <ListItem button onClick={() => handleClick(v.label)}>
+                  {v.icon && (
+                    <ListItemIcon>
+                      <IconComponent />
+                    </ListItemIcon>
+                  )}
+
+                  <ListItemText primary={v.label} />
+                  {open[v.label] ? <ExpandLess /> : <ExpandMore />}
+                </ListItem>
+                <Collapse in={open[v.label]} timeout="auto" unmountOnExit>
+                  <List component="div" disablePadding>
+                    <NestList data={v.children} className={classes.nested} />
+                  </List>
+                </Collapse>
+              </div>
+            );
+          }
+          return (
+            <ListItem
+              button
+              key={v.label}
+              className={`${className || ''} ${classes.item} ${
+                active === v.label ? classes.active : ''
+              }`}
+              onClick={() => {
+                setActive(v.label);
+                v.onClick && v.onClick();
+              }}
+            >
+              {v.icon && (
+                <ListItemIcon className={classes.itemIcon}>
+                  <IconComponent />
+                </ListItemIcon>
+              )}
+
+              <ListItemText primary={v.label} />
+            </ListItem>
+          );
+        })}
+      </>
+    );
+  };
+
+  return (
+    <List component="nav" className={classes.root}>
+      <div>
+        <div className={classes.logoWrapper}>
+          <img className={classes.logo} src={logoPath} alt="cloud logo" />
+        </div>
+
+        <NestList data={data} />
+      </div>
+    </List>
+  );
+};
+
+export default NavMenu;

+ 71 - 0
client/src/components/menu/SimpleMenu.tsx

@@ -0,0 +1,71 @@
+import React, { FC, useMemo } from 'react';
+import Menu from '@material-ui/core/Menu';
+import MenuItem from '@material-ui/core/MenuItem';
+import { generateId } from '../../utils/Common';
+import { SimpleMenuType } from './Types';
+import CustomButton from '../customButton/CustomButton';
+import { makeStyles, Theme } from '@material-ui/core';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  menuItem: {
+    minWidth: '160px',
+  },
+}));
+
+const SimpleMenu: FC<SimpleMenuType> = props => {
+  const { label, menuItems, buttonProps, className = '' } = props;
+  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
+
+  const classes = getStyles();
+  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
+    setAnchorEl(event.currentTarget);
+  };
+
+  const handleClose = () => {
+    setAnchorEl(null);
+  };
+  // for accessibility
+  const id = useMemo(() => generateId(), []);
+
+  return (
+    <div className={className}>
+      <CustomButton
+        aria-controls={id}
+        aria-haspopup="true"
+        onClick={handleClick}
+        {...buttonProps}
+      >
+        {label}
+      </CustomButton>
+      <Menu
+        id={id}
+        anchorEl={anchorEl}
+        keepMounted
+        open={Boolean(anchorEl)}
+        onClose={handleClose}
+        getContentAnchorEl={null}
+        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+        transformOrigin={{ vertical: 'top', horizontal: 'center' }}
+      >
+        {menuItems.map((v, i) =>
+          typeof v.label === 'string' ? (
+            <MenuItem
+              classes={{ root: classes.menuItem }}
+              onClick={() => {
+                v.callback && v.callback();
+                handleClose();
+              }}
+              key={v.label + i}
+            >
+              {v.label}
+            </MenuItem>
+          ) : (
+            <span key={i}>{v.label}</span>
+          )
+        )}
+      </Menu>
+    </div>
+  );
+};
+
+export default SimpleMenu;

+ 25 - 0
client/src/components/menu/Types.ts

@@ -0,0 +1,25 @@
+import { ButtonProps } from '@material-ui/core/Button';
+import { ReactElement } from 'react';
+
+export type SimpleMenuType = {
+  label: string;
+  menuItems: { label: string | ReactElement; callback?: () => void }[];
+  buttonProps?: ButtonProps;
+  className?: string;
+};
+
+export type NavMenuItem = {
+  icon: (
+    props?: any
+  ) => React.ReactElement<any, string | React.JSXElementConstructor<any>>;
+  label: String;
+  onClick?: () => void;
+  children?: NavMenuItem[];
+};
+
+export type NavMenuType = {
+  defaultActive?: string;
+  defaultOpen?: { [x: string]: boolean };
+  width: string;
+  data: NavMenuItem[];
+};

+ 89 - 0
client/src/components/status/Status.tsx

@@ -0,0 +1,89 @@
+import React, { FC, useMemo } from 'react';
+import { StatusType, StatusEnum } from './Types';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme, createStyles, Typography } from '@material-ui/core';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      display: 'flex',
+      alignItems: 'center',
+    },
+    label: {
+      fontWeight: 'bold',
+      textTransform: 'uppercase',
+    },
+    circle: {
+      backgroundColor: (props: any) => props.color,
+      borderRadius: '50%',
+      width: '10px',
+      height: '10px',
+      marginRight: theme.spacing(0.5),
+    },
+
+    flash: {
+      animation: '$bgColorChange 1.5s infinite',
+    },
+
+    '@keyframes bgColorChange': {
+      '0%': {
+        backgroundColor: (props: any) => props.color,
+      },
+      '50%': {
+        backgroundColor: 'transparent',
+      },
+      '100%': {
+        backgroundColor: (props: any) => props.color,
+      },
+    },
+  })
+);
+
+const Status: FC<StatusType> = props => {
+  const { status } = props;
+  const { t } = useTranslation();
+  const statusTrans: { [key in string]: string } = t('status');
+  const { label, color } = useMemo(() => {
+    switch (status) {
+      case StatusEnum.creating:
+        return {
+          label: statusTrans.creating,
+          color: '#06aff2',
+        };
+
+      case StatusEnum.running:
+        return {
+          label: statusTrans.running,
+          color: '#06f3af',
+        };
+      case StatusEnum.error:
+        return {
+          label: statusTrans.error,
+          color: '#f25c06',
+        };
+
+      default:
+        return {
+          label: statusTrans.error,
+          color: '#f25c06',
+        };
+    }
+  }, [status, statusTrans]);
+
+  const classes = useStyles({ color });
+
+  return (
+    <div className={classes.root}>
+      <div
+        className={`${classes.circle} ${
+          status === StatusEnum.creating ? classes.flash : ''
+        }`}
+      ></div>
+      <Typography variant="body2" className={classes.label}>
+        {label}
+      </Typography>
+    </div>
+  );
+};
+
+export default Status;

+ 44 - 0
client/src/components/status/StatusIcon.tsx

@@ -0,0 +1,44 @@
+import { CircularProgress, makeStyles, Theme } from '@material-ui/core';
+import React, { FC, ReactElement } from 'react';
+import { getStatusType } from '../../utils/Status';
+import icons from '../icons/Icons';
+import { StatusEnum, StatusType } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+    justifyContent: 'flex-left',
+    alignItems: 'center',
+    paddingLeft: theme.spacing(1),
+  },
+  svg: {
+    color: theme.palette.primary.main,
+  },
+}));
+
+const StatusIcon: FC<StatusType> = props => {
+  const classes = useStyles();
+  const { status } = props;
+
+  const getElement = (status: StatusEnum): ReactElement => {
+    const type = getStatusType(status);
+    switch (type) {
+      case 'loading':
+        return (
+          <CircularProgress
+            size={24}
+            thickness={8}
+            classes={{ svg: classes.svg }}
+          />
+        );
+      case 'success':
+        return icons.success({ style: { color: '#34b78f' } });
+      default:
+        return icons.error({ style: { color: '#fc4c02' } });
+    }
+  };
+
+  return <div className={classes.wrapper}>{getElement(status)}</div>;
+};
+
+export default StatusIcon;

+ 8 - 0
client/src/components/status/Types.ts

@@ -0,0 +1,8 @@
+export enum StatusEnum {
+  'creating',
+  'running',
+  'error',
+}
+export type StatusType = {
+  status: StatusEnum;
+};

+ 306 - 0
client/src/components/textField/CustomInput.tsx

@@ -0,0 +1,306 @@
+import {
+  FilledTextFieldProps,
+  FormControl,
+  FormHelperText,
+  Grid,
+  IconButton,
+  Input,
+  InputAdornment,
+  InputLabel,
+  makeStyles,
+  StandardTextFieldProps,
+  TextField,
+} from '@material-ui/core';
+import Icons from '../icons/Icons';
+import React, { ReactElement } from 'react';
+import {
+  IAdornmentConfig,
+  IIconConfig,
+  ITextfieldConfig,
+  ICustomInputProps,
+  IBlurParam,
+  IValidInfo,
+  IChangeParam,
+} from './Types';
+
+const handleOnBlur = (param: IBlurParam) => {
+  const {
+    event,
+    key,
+    param: { cb, checkValid, validations },
+  } = param;
+  const input = event.target.value;
+  const isValid = validations
+    ? checkValid({
+        key,
+        value: input,
+        rules: validations,
+      })
+    : true;
+
+  if (isValid) {
+    cb(input);
+  }
+};
+
+const handleOnChange = (param: IChangeParam) => {
+  const {
+    event,
+    key,
+    param: { cb, checkValid, validations },
+  } = param;
+  const input = event.target.value;
+  const isValid = validations
+    ? checkValid({
+        key,
+        value: input,
+        rules: validations,
+      })
+    : true;
+
+  if (isValid) {
+    cb(input);
+  }
+};
+
+const getAdornmentStyles = makeStyles(theme => ({
+  icon: {
+    color: '#82838e',
+  },
+}));
+
+const getAdornmentInput = (
+  config: IAdornmentConfig,
+  checkValid: Function,
+  validInfo: IValidInfo
+): ReactElement => {
+  const {
+    label,
+    key,
+    icon,
+    isPasswordType = false,
+    inputClass,
+    showPassword,
+    validations,
+    onIconClick,
+    onInputBlur,
+    onInputChange,
+  } = config;
+
+  const classes = getAdornmentStyles();
+
+  const param = {
+    cb: onInputBlur || (() => {}),
+    validations: validations || [],
+    checkValid,
+  };
+
+  const info = validInfo ? validInfo[key] : null;
+
+  return (
+    <FormControl>
+      <InputLabel htmlFor="standard-adornment-password">{label}</InputLabel>
+      <Input
+        classes={{ root: `${inputClass || {}}` }}
+        type={isPasswordType ? (showPassword ? 'text' : 'password') : 'text'}
+        onBlur={e => {
+          handleOnBlur({ event: e, key, param });
+        }}
+        onChange={e => {
+          handleOnChange({
+            event: e,
+            key,
+            param: {
+              ...param,
+              cb: onInputChange || (() => {}),
+            },
+          });
+        }}
+        endAdornment={
+          <InputAdornment position="end">
+            <IconButton onClick={onIconClick || (() => {})} edge="end">
+              {isPasswordType
+                ? showPassword
+                  ? Icons.visible({ classes: { root: classes.icon } })
+                  : Icons.invisible({ classes: { root: classes.icon } })
+                : icon}
+            </IconButton>
+          </InputAdornment>
+        }
+        inputProps={{
+          'data-cy': key,
+        }}
+      />
+
+      {
+        <FormHelperText classes={{ root: `${inputClass || {}}` }}>
+          {info && info.result && info.errText
+            ? createHelperTextNode(info.errText)
+            : ' '}
+        </FormHelperText>
+      }
+    </FormControl>
+  );
+};
+
+const getIconInput = (
+  config: IIconConfig,
+  checkValid: Function,
+  validInfo: IValidInfo
+): ReactElement => {
+  const {
+    icon,
+    inputType,
+    inputConfig,
+    containerClass,
+    spacing,
+    alignItems,
+  } = config;
+  return (
+    <Grid
+      classes={{ container: `${containerClass || {}}` }}
+      container
+      spacing={spacing || 0}
+      alignItems={alignItems}
+    >
+      <Grid item>{icon}</Grid>
+      <Grid item>
+        {inputType === 'icon'
+          ? getTextfield(inputConfig as ITextfieldConfig, checkValid, validInfo)
+          : getAdornmentInput(
+              inputConfig as IAdornmentConfig,
+              checkValid,
+              validInfo
+            )}
+      </Grid>
+    </Grid>
+  );
+};
+
+const getTextfield = (
+  config: ITextfieldConfig,
+  checkValid: Function,
+  validInfo: IValidInfo
+): ReactElement => {
+  const {
+    key,
+    className,
+    validations,
+    onBlur,
+    onChange,
+    fullWidth,
+    size,
+    placeholder,
+    inputProps,
+    InputProps,
+    value,
+    ...others
+  } = config;
+
+  if (value !== undefined) {
+    (others as any).value = value;
+  }
+
+  const param = {
+    cb: onBlur || (() => {}),
+    validations: validations || [],
+    checkValid,
+  };
+
+  const info = validInfo ? validInfo[key] : null;
+  const defaultInputProps = { 'data-cy': key };
+  return (
+    <TextField
+      {...(others as
+        | StandardTextFieldProps
+        | FilledTextFieldProps
+        | FilledTextFieldProps)}
+      size={size || 'medium'}
+      fullWidth={fullWidth}
+      placeholder={placeholder || ''}
+      inputProps={
+        inputProps
+          ? { ...inputProps, ...defaultInputProps }
+          : { ...defaultInputProps }
+      }
+      InputProps={InputProps ? { ...InputProps } : {}}
+      helperText={
+        info && info.result && info.errText
+          ? createHelperTextNode(info.errText)
+          : ' '
+      }
+      className={className || ''}
+      onBlur={event => {
+        handleOnBlur({ event, key, param });
+      }}
+      // value={value}
+      onChange={event => {
+        handleOnChange({
+          event,
+          key,
+          param: { ...param, cb: onChange || (() => {}) },
+        });
+      }}
+    />
+  );
+};
+
+const getStyles = makeStyles(theme => ({
+  errWrapper: {
+    display: 'flex',
+    alignItems: 'flex-start',
+    color: `${theme.palette.error.main}`,
+    wordWrap: 'break-word',
+    wordBreak: 'break-all',
+    overflow: 'hidden',
+  },
+  errBtn: {
+    marginRight: `${theme.spacing(1)}`,
+  },
+}));
+
+const createHelperTextNode = (hint: string): ReactElement => {
+  const classes = getStyles();
+  return (
+    <span className={classes.errWrapper}>
+      {Icons.error({
+        fontSize: 'small',
+        classes: {
+          root: classes.errBtn,
+        },
+      })}
+      {hint}
+    </span>
+  );
+};
+
+const CustomInput = (props: ICustomInputProps) => {
+  const {
+    type,
+    iconConfig,
+    textConfig,
+    adornmentConfig,
+    checkValid,
+    validInfo,
+  } = props;
+
+  let template: ReactElement | null;
+  switch (type) {
+    case 'adornment':
+      template = getAdornmentInput(adornmentConfig!, checkValid!, validInfo!);
+      break;
+    case 'icon':
+      template = getIconInput(iconConfig!, checkValid!, validInfo!);
+      break;
+    case 'text':
+      template = getTextfield(textConfig!, checkValid!, validInfo!);
+      break;
+    default:
+      // default is plain text input
+      template = getTextfield(textConfig!, checkValid!, validInfo!);
+  }
+
+  return template;
+};
+
+export default CustomInput;

+ 142 - 0
client/src/components/textField/SearchInput.tsx

@@ -0,0 +1,142 @@
+import { InputAdornment, makeStyles, TextField } from '@material-ui/core';
+import React, { FC, useState } from 'react';
+import Icons from '../icons/Icons';
+import { SearchType } from './Types';
+
+const useSearchStyles = makeStyles(theme => ({
+  wrapper: {
+    display: 'flex',
+  },
+  input: {
+    backgroundColor: '#fff',
+    borderRadius: '4px 4px 0 0',
+    padding: (props: any) => `${props.showInput ? theme.spacing(0, 1) : 0}`,
+    boxSizing: 'border-box',
+    width: (props: any) => `${props.showInput ? '255px' : 0}`,
+    transition: 'all 0.2s',
+
+    '& .MuiAutocomplete-endAdornment': {
+      right: theme.spacing(0.5),
+    },
+
+    '& .MuiInput-underline:before': {
+      border: 'none',
+    },
+
+    '& .MuiInput-underline:after': {
+      border: 'none',
+    },
+  },
+  searchIcon: {
+    paddingLeft: theme.spacing(1),
+    color: theme.palette.primary.main,
+    cursor: 'pointer',
+    fontSize: '24px',
+  },
+  clearIcon: {
+    color: 'rgba(0, 0, 0, 0.6)',
+    cursor: 'pointer',
+  },
+  iconWrapper: {
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'center',
+
+    opacity: (props: any) => `${props.searched ? 1 : 0}`,
+    transition: 'opacity 0.2s',
+  },
+  searchWrapper: {
+    display: (props: any) => `${props.showInput ? 'flex' : 'none'}`,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+}));
+
+const SearchInput: FC<SearchType> = props => {
+  const { searchText = '', onClear = () => {}, onSearch = () => {} } = props;
+  const [searchValue, setSearchValue] = useState<string>(searchText);
+  const searched = searchValue !== '';
+  const [showInput, setShowInput] = useState<boolean>(searchValue !== '');
+
+  const classes = useSearchStyles({ searched, showInput });
+
+  const inputRef = React.useRef<any>(null);
+
+  const onIconClick = () => {
+    setShowInput(true);
+    if (inputRef.current) {
+      inputRef.current.focus();
+    }
+
+    if (searched) {
+      onSearch(searchValue);
+    }
+  };
+
+  const handleInputBlur = () => {
+    setShowInput(searched);
+  };
+
+  return (
+    <div className={classes.wrapper}>
+      {!showInput && (
+        <div className={classes.searchIcon} onClick={onIconClick}>
+          {Icons.search()}
+        </div>
+      )}
+      <TextField
+        inputRef={inputRef}
+        autoFocus={true}
+        variant="standard"
+        classes={{ root: classes.input }}
+        InputProps={{
+          disableUnderline: true,
+          endAdornment: (
+            <InputAdornment position="end">
+              <span
+                className={classes.iconWrapper}
+                onClick={e => {
+                  setSearchValue('');
+                  inputRef.current.focus();
+                  onClear();
+                }}
+              >
+                {Icons.clear({ classes: { root: classes.clearIcon } })}
+              </span>
+            </InputAdornment>
+          ),
+          startAdornment: (
+            <InputAdornment position="start">
+              <span
+                className={classes.searchWrapper}
+                onClick={() => onSearch(searchValue)}
+              >
+                {Icons.search({ classes: { root: classes.searchIcon } })}
+              </span>
+            </InputAdornment>
+          ),
+        }}
+        onBlur={handleInputBlur}
+        onChange={e => {
+          // console.log('change', e.target.value);
+          const value = e.target.value;
+          setSearchValue(value);
+          if (value === '') {
+            onClear();
+          }
+        }}
+        onKeyPress={e => {
+          // console.log(`Pressed keyCode ${e.key}`);
+          if (e.key === 'Enter') {
+            // Do code here
+            onSearch(searchValue);
+            e.preventDefault();
+          }
+        }}
+        value={searchValue}
+      />
+    </div>
+  );
+};
+
+export default SearchInput;

+ 100 - 0
client/src/components/textField/Types.ts

@@ -0,0 +1,100 @@
+import { ReactElement } from 'react';
+import { IValidationItem } from '../../hooks/Form';
+import { IExtraParam, ValidType } from '../../utils/Validation';
+
+export type InputType = 'icon' | 'adornment' | 'text' | undefined;
+export type VariantType = 'filled' | 'outlined' | 'standard';
+export type SizeType = 'small' | 'medium' | undefined;
+
+export type AlignItemsType =
+  | 'baseline'
+  | 'center'
+  | 'stretch'
+  | 'flex-start'
+  | 'flex-end'
+  | undefined;
+
+export interface IValidation {
+  rule: ValidType;
+  // extra params for some special check like confirm
+  extraParam?: IExtraParam;
+  errorText: string;
+}
+
+export interface IBlurParam {
+  event: any;
+  key: string;
+  param: {
+    cb: Function;
+    checkValid: Function;
+    validations: IValidation[];
+  };
+}
+
+export interface IChangeParam extends IBlurParam {}
+
+export interface ICustomInputProps {
+  type?: InputType;
+  iconConfig?: IIconConfig;
+  adornmentConfig?: IAdornmentConfig;
+  textConfig?: ITextfieldConfig;
+
+  // used for validation
+  checkValid?: Function;
+  validInfo?: IValidInfo;
+}
+
+export interface IValidInfo {
+  [key: string]: IValidationItem;
+}
+
+export interface IIconConfig {
+  icon: ReactElement;
+  inputType: InputType;
+  inputConfig: ITextfieldConfig | IAdornmentConfig;
+  containerClass?: string;
+  spacing?: any;
+  alignItems?: AlignItemsType;
+  iconClass?: string;
+}
+
+export interface ITextfieldConfig {
+  variant: VariantType;
+  value?: any;
+  label?: string;
+  hiddenLabel?: boolean;
+  size?: SizeType;
+  placeholder?: string;
+  required?: boolean;
+  disabled?: boolean;
+  defaultValue?: any;
+  classes?: any;
+  InputProps?: any;
+  inputProps?: any;
+  key: string;
+  validations?: IValidation[];
+  fullWidth?: boolean;
+  className?: string;
+  type?: string;
+  onBlur?: (event: any) => void;
+  onChange?: (event: any) => void;
+}
+
+export interface IAdornmentConfig {
+  label: string;
+  key: string;
+  icon?: ReactElement;
+  isPasswordType?: boolean;
+  inputClass?: string;
+  showPassword?: boolean;
+  validations?: IValidation[];
+  onIconClick?: () => void;
+  onInputBlur?: (event: any) => void;
+  onInputChange?: (event: any) => void;
+}
+
+export type SearchType = {
+  searchText?: string;
+  onClear?: () => void;
+  onSearch: (value: string) => void;
+};

+ 7 - 0
client/src/consts/Http.ts

@@ -0,0 +1,7 @@
+export enum CODE_STATUS {
+  SUCCESS = 0,
+  UNAUTHORIZED = 401,
+  UNVERIFIED = 20305,
+}
+
+export const START_LOADING_TIME = 350;

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

@@ -0,0 +1,3 @@
+export const TOKEN = 'milvusToken';
+export const SESSION = 'CLOUD_SESSION';
+export const USER_INFO = 'userInfo';

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

@@ -0,0 +1,141 @@
+export const VECTOR_TYPE_OPTIONS = [
+  {
+    label: 'Vector float',
+    value: 'VECTOR_FLOAT',
+  },
+  {
+    label: 'Vector binary',
+    value: 'VECTOR_BINARY',
+  },
+];
+
+export const NON_VECTOR_TYPE_OPTIONS = [
+  {
+    label: 'Number',
+    value: 'number',
+  },
+  {
+    label: 'Float',
+    value: 'float',
+  },
+];
+
+export enum METRIC_TYPES_VALUES {
+  L2 = 1,
+  IP,
+  HAMMING,
+  JACCARD,
+  TANIMOTO,
+  SUBSTRUCTURE,
+  SUPERSTRUCTURE,
+}
+
+export const METRIC_TYPES = [
+  {
+    value: METRIC_TYPES_VALUES.L2,
+    label: 'L2',
+  },
+  {
+    value: METRIC_TYPES_VALUES.IP,
+    label: 'IP',
+  },
+  {
+    value: METRIC_TYPES_VALUES.HAMMING,
+    label: 'Hamming',
+  },
+  {
+    value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
+    label: 'Substructure',
+  },
+  {
+    value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
+    label: 'Superstructure',
+  },
+  {
+    value: METRIC_TYPES_VALUES.JACCARD,
+    label: 'Jaccard',
+  },
+  {
+    value: METRIC_TYPES_VALUES.TANIMOTO,
+    label: 'Tanimoto',
+  },
+];
+
+export const BINARY_METRIC_TYPES = [
+  'HAMMING',
+  'JACCARD',
+  'TANIMOTO',
+  'SUBSTRUCTURE',
+  'SUPERSTRUCTURE',
+];
+
+export type searchKeywordsType = 'nprobe' | 'ef' | 'search_k' | 'search_length';
+
+// index
+export const INDEX_CONFIG: {
+  [x: string]: {
+    create: string[];
+    search: searchKeywordsType[];
+  };
+} = {
+  IVF_FLAT: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+  IVF_PQ: {
+    create: ['nlist', 'm'],
+    search: ['nprobe'],
+  },
+  IVF_SQ8: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+  IVF_SQ8_HYBRID: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+  FLAT: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+  HNSW: {
+    create: ['M', 'efConstruction'],
+    search: ['ef'],
+  },
+  ANNOY: {
+    create: ['n_trees'],
+    search: ['search_k'],
+  },
+  RNSG: {
+    create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
+    search: ['search_length'],
+  },
+};
+
+export const COLLECTION_NAME_REGX = /^[0-9,a-z,A-Z$_]+$/;
+
+export const m_OPTIONS = [
+  { label: '64', value: 64 },
+  { label: '32', value: 32 },
+  { label: '16', value: 16 },
+  { label: '8', value: 8 },
+  { label: '4', value: 4 },
+];
+
+export const INDEX_OPTIONS_MAP = {
+  FLOAT_POINT: Object.keys(INDEX_CONFIG).map(v => ({ label: v, value: v })),
+  BINARY_ONE: [{ label: 'FLAT', value: 'FLAT' }],
+  BINARY_TWO: [
+    { label: 'FLAT', value: 'FLAT' },
+    { label: 'IVF_FLAT', value: 'IVF_FLAT' },
+  ],
+};
+
+export const FIELD_TYPES = {
+  VECTOR_FLOAT: 'vector_float',
+  VECTOR_BINARY: 'vector_binary',
+  Float: 'float',
+  Double: 'double',
+  INT32: 'int32',
+  INT64: 'int64',
+};

+ 6 - 0
client/src/consts/Polling.ts

@@ -0,0 +1,6 @@
+export const POLLING_INTERVAL = 1000 * 30;
+
+export enum PollingTypeEnum {
+  databases = 'DATABASES',
+  queries = 'QUERY_SERVICES',
+}

+ 6 - 0
client/src/consts/Reducer.ts

@@ -0,0 +1,6 @@
+// reducer actions
+export const ADD = 'add';
+export const UPDATE = 'update';
+export const INIT = 'init';
+export const DELETE = 'delete';
+export const RESET = 'reset';

+ 6 - 0
client/src/consts/Util.ts

@@ -0,0 +1,6 @@
+export const BYTE_UNITS: { [x: string]: number } = {
+  b: 1,
+  k: 1024,
+  m: 1024 * 1024,
+  g: 1024 * 1024 * 1024,
+};

+ 1 - 0
client/src/consts/WebSocket.ts

@@ -0,0 +1 @@
+export const CONNECT = 'connect';

+ 281 - 0
client/src/context/Root.tsx

@@ -0,0 +1,281 @@
+import React, { useState, useCallback } from 'react';
+import {
+  // for strict mode
+  unstable_createMuiStrictModeTheme as createMuiTheme,
+  ThemeProvider,
+  makeStyles,
+} from '@material-ui/core/styles';
+import { Backdrop, CircularProgress, SwipeableDrawer } from '@material-ui/core';
+
+import {
+  RootContextType,
+  DialogType,
+  SnackBarType,
+  OpenSnackBarType,
+} from './Types';
+import CustomSnackBar from '../components/customSnackBar/CustomSnackBar';
+import CustomDialog from '../components/customDialog/CustomDialog';
+import lightBlue from '@material-ui/core/colors/lightBlue';
+
+declare module '@material-ui/core/styles/createPalette' {
+  interface Palette {
+    zilliz: Palette['primary'];
+  }
+  interface PaletteOptions {
+    zilliz: PaletteOptions['primary'];
+  }
+}
+
+const DefaultDialogConfigs: DialogType = {
+  open: false,
+  type: 'notice',
+  params: {
+    title: '',
+    component: <></>,
+    confirm: () => new Promise((res, rej) => res(true)),
+    cancel: () => new Promise((res, rej) => res(true)),
+  },
+};
+
+export const rootContext = React.createContext<RootContextType>({
+  openSnackBar: (
+    message,
+    type = 'success',
+    autoHideDuration,
+    position = { vertical: 'top', horizontal: 'right' }
+  ) => {},
+  dialog: DefaultDialogConfigs,
+  setDialog: params => {},
+  handleCloseDialog: () => {},
+  setGlobalLoading: () => {},
+  setDrawer: (params: any) => {},
+});
+
+const otherThemes = {
+  spacing: (factor: any) => `${8 * factor}px`,
+};
+
+const theme = createMuiTheme({
+  palette: {
+    primary: {
+      ...lightBlue,
+      main: '#06AFF2',
+      light: '#65DAF8',
+      dark: '#009BC4',
+    },
+    secondary: {
+      light: '#82D3BA',
+      main: '#31B78D',
+      dark: '#279371',
+    },
+    error: {
+      main: '#FF4605',
+      light: '#FF8F68',
+      dark: '#CD3804',
+    },
+    zilliz: {
+      ...lightBlue,
+      light: lightBlue[50],
+    },
+  },
+  ...otherThemes,
+  overrides: {
+    MuiTypography: {
+      button: {
+        textTransform: 'initial',
+        lineHeight: '16px',
+        fontWeight: 'bold',
+      },
+      h1: {
+        fontSize: '36px',
+        lineHeight: '42px',
+        letterSpacing: '-0.02em',
+      },
+      h2: {
+        lineHeight: '24px',
+        fontSize: '28px',
+      },
+      h3: {
+        lineHeight: '20px',
+        fontSize: '23px',
+        fontWeight: 'bold',
+      },
+      h4: {
+        fontWeight: 500,
+        lineHeight: '23px',
+        fontSize: '20px',
+        letterSpacing: '-0.02em',
+      },
+      h5: {
+        fontWeight: 'bold',
+        fontSize: '16px',
+        lineHeight: '24px',
+      },
+      h6: {
+        fontWeight: 'normal',
+        fontSize: '16px',
+        lineHeight: '24px',
+        letterSpacing: '-0.01em',
+      },
+      body1: {
+        fontSize: '14px',
+        lineHeight: '20px',
+      },
+      body2: {
+        fontSize: '12px',
+        lineHeight: '16px',
+      },
+      caption: {
+        fontSize: '10px',
+        lineHeight: '12px',
+      },
+    },
+    MuiButton: {
+      root: {
+        textTransform: 'initial',
+        fontWeight: 'bold',
+      },
+      text: {
+        '&:hover': {
+          backgroundColor: lightBlue[50],
+        },
+      },
+    },
+    MuiDialogActions: {
+      spacing: {
+        padding: otherThemes.spacing(4),
+      },
+    },
+    MuiDialogContent: {
+      root: {
+        padding: `${otherThemes.spacing(1)} ${otherThemes.spacing(4)}`,
+      },
+    },
+    MuiDialogTitle: {
+      root: {
+        padding: otherThemes.spacing(4),
+        paddingBottom: otherThemes.spacing(1),
+      },
+    },
+    MuiStepIcon: {
+      root: {
+        color: '#c4c4c4',
+        '&$active': {
+          color: '#12C3F4',
+        },
+        '&$completed': {
+          color: '#12C3F4',
+        },
+      },
+    },
+    MuiFormHelperText: {
+      contained: {
+        marginLeft: 0,
+      },
+    },
+  },
+});
+
+const { Provider } = rootContext;
+// Dialog has two type : normal | custom;
+// notice type mean it's a notice dialog you need to set props like title, content, actions
+// custom type could have own state, you could set a complete component in dialog.
+export const RootProvider = (props: { children: React.ReactNode }) => {
+  const classes = makeStyles({
+    paper: {
+      minWidth: '300px',
+      borderRadius: '0px',
+    },
+    paperAnchorRight: {
+      width: '40vw',
+    },
+  })();
+  const [snackBar, setSnackBar] = useState<SnackBarType>({
+    open: false,
+    type: 'success',
+    message: '',
+    vertical: 'top',
+    horizontal: 'right',
+    autoHideDuration: 3000,
+  });
+  const [dialog, setDialog] = useState<DialogType>(DefaultDialogConfigs);
+  const [globalLoading, setGlobalLoading] = useState<boolean>(false);
+  const [drawer, setDrawer]: any = useState({
+    anchor: 'right',
+    open: false,
+    child: <></>,
+  });
+
+  const handleSnackBarClose = () => {
+    setSnackBar(v => ({ ...v, open: false }));
+  };
+  const openSnackBar: OpenSnackBarType = useCallback(
+    (
+      message = '',
+      type = 'success',
+      autoHideDuration: number | null | undefined = 5000,
+      position = { vertical: 'top', horizontal: 'right' }
+    ) => {
+      setSnackBar({
+        open: true,
+        message,
+        type,
+        autoHideDuration,
+        ...position,
+      });
+    },
+    []
+  );
+  const handleCloseDialog = () => {
+    // setDialog(DefaultDialogConfigs);
+    setDialog({
+      ...dialog,
+      open: false,
+    });
+  };
+
+  const toggleDrawer =
+    (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
+      if (
+        event.type === 'keydown' &&
+        ((event as React.KeyboardEvent).key === 'Tab' ||
+          (event as React.KeyboardEvent).key === 'Shift')
+      ) {
+        return;
+      }
+
+      setDrawer({ ...drawer, open: open });
+    };
+
+  return (
+    <Provider
+      value={{
+        openSnackBar,
+        dialog,
+        setDialog,
+        handleCloseDialog,
+        setGlobalLoading,
+        setDrawer,
+      }}
+    >
+      <ThemeProvider theme={theme}>
+        <CustomSnackBar {...snackBar} onClose={handleSnackBarClose} />
+        {props.children}
+        <CustomDialog {...dialog} onClose={handleCloseDialog} />
+        <Backdrop open={globalLoading} style={{ zIndex: 2000 }}>
+          <CircularProgress color="inherit" />
+        </Backdrop>
+
+        <SwipeableDrawer
+          anchor={drawer.anchor}
+          open={drawer.open}
+          onClose={toggleDrawer(false)}
+          onOpen={toggleDrawer(true)}
+          classes={{ paperAnchorRight: classes.paperAnchorRight }}
+        >
+          {drawer.child}
+        </SwipeableDrawer>
+      </ThemeProvider>
+    </Provider>
+  );
+};

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

@@ -0,0 +1,52 @@
+import { ReactElement } from 'react';
+
+export type RootContextType = {
+  openSnackBar: OpenSnackBarType;
+  dialog: DialogType;
+  setDialog: (params: DialogType) => void;
+  handleCloseDialog: () => void;
+  setGlobalLoading: (loading: boolean) => void;
+  setDrawer: (params: any) => void;
+};
+
+// this is for any custom dialog
+export type DialogType = {
+  open: boolean;
+  type: 'notice' | 'custom';
+  params: {
+    title?: string;
+    component?: React.ReactNode;
+    confirm?: () => Promise<any>;
+    cancel?: () => Promise<any>;
+    confirmLabel?: string;
+    cancelLabel?: string;
+    confirmClass?: string;
+    /**
+     * Usually we control open status in root context,
+     * if we need a hoc component depend on setDialog in context,
+     * we may need control open status by ourself
+     *  */
+    handleClose?: () => void;
+
+    // used for dialog position
+    containerClass?: string;
+  };
+};
+export type SnackBarType = {
+  open: boolean;
+  message: string | ReactElement;
+  type?: 'error' | 'info' | 'success' | 'warning';
+  autoHideDuration?: number | null;
+  horizontal: 'center' | 'left' | 'right';
+  vertical: 'bottom' | 'top';
+};
+
+export type OpenSnackBarType = (
+  message: string | ReactElement,
+  type?: 'error' | 'info' | 'success' | 'warning',
+  autoHideDuration?: number | null,
+  position?: {
+    horizontal: 'center' | 'left' | 'right';
+    vertical: 'bottom' | 'top';
+  }
+) => void;

+ 37 - 0
client/src/hooks/CommonStyle.ts

@@ -0,0 +1,37 @@
+import { makeStyles } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+  root: {
+    padding: `${theme.spacing(4)}px ${theme.spacing(4)}px`,
+    backgroundColor: '#f4f4f4',
+  },
+  titleWrapper: {
+    background: theme.palette.primary.light,
+    color: '#fff',
+    padding: theme.spacing(2),
+    '& h2': {
+      fontSize: '26px',
+    },
+  },
+  paper: {
+    color: theme.palette.text.secondary,
+    padding: theme.spacing(4),
+  },
+  titleContainer: {
+    display: 'flex',
+    padding: theme.spacing(1, 0, 2),
+  },
+  h2: {
+    fontSize: '26px',
+    fontWeight: 'bold',
+    margin: '0 10px 0 0',
+  },
+  tab: {
+    background: theme.palette.primary.main,
+    color: '#fff',
+  },
+}));
+
+export function usePageStyles() {
+  return useStyles();
+}

+ 129 - 0
client/src/hooks/Form.ts

@@ -0,0 +1,129 @@
+import { useState } from 'react';
+import { IValidation } from '../components/textField/Types';
+import { checkIsEmpty, getCheckResult } from '../utils/Validation';
+
+export interface IForm {
+  key: string;
+  value?: any;
+  needCheck?: boolean;
+}
+
+interface IValidationInfo {
+  // check general validation
+  checkFormValid: Function;
+  // check detail validation
+  checkIsValid: Function;
+  validation: {
+    [key: string]: IValidationItem;
+  };
+  disabled: boolean;
+  setDisabled: Function;
+  resetValidation: (form: IForm[]) => void;
+}
+
+export interface IValidationItem {
+  result: boolean;
+  errText: string;
+}
+
+export interface ICheckValidParam {
+  value: string;
+  key: string;
+  // one input can contains multiple rules
+  rules: IValidation[];
+}
+
+export const useFormValidation = (form: IForm[]): IValidationInfo => {
+  const initValidation = form
+    .filter(f => f.needCheck)
+    .reduce(
+      (acc, cur) => ({
+        ...acc,
+        [cur.key]: {
+          result:
+            typeof cur.value === 'string' ? cur.value === '' : cur.value === 0,
+          errText: '',
+        },
+      }),
+      {}
+    );
+
+  console.log('init validation', initValidation);
+
+  // validation detail about form item
+  const [validation, setValidation] = useState(initValidation);
+  // overall validation result to control following actions
+  const [disabled, setDisabled] = useState<boolean>(true);
+
+  const checkIsValid = (param: ICheckValidParam): IValidationItem => {
+    const { value, key, rules } = param;
+
+    let validDetail = {
+      result: false,
+      errText: '',
+    };
+
+    for (let i = 0; i < rules.length; i++) {
+      const rule = rules[i];
+      const checkResult = getCheckResult({
+        value,
+        extraParam: rule.extraParam,
+        rule: rule.rule,
+      });
+      if (!checkResult) {
+        validDetail = {
+          result: true,
+          errText: rule.errorText || '',
+        };
+
+        break;
+      }
+    }
+
+    const validInfo = {
+      ...validation,
+      [key]: validDetail,
+    };
+
+    const isOverallValid = Object.values(validInfo).every(
+      v => !(v as IValidationItem).result
+    );
+
+    setDisabled(!isOverallValid);
+    setValidation(validInfo);
+
+    return validDetail;
+  };
+
+  const checkFormValid = (form: IForm[]): boolean => {
+    const requireCheckItems = form.filter(f => f.needCheck);
+    if (requireCheckItems.some(item => !checkIsEmpty(item.value))) {
+      return false;
+    }
+
+    const validations = Object.values(validation);
+    return validations.every(v => !(v as IValidationItem).result);
+  };
+
+  const resetValidation = (form: IForm[]) => {
+    const validation = form
+      .filter(f => f.needCheck)
+      .reduce(
+        (acc, cur) => ({
+          ...acc,
+          [cur.key]: { result: cur.value === '', errText: '' },
+        }),
+        {}
+      );
+    setValidation(validation);
+  };
+
+  return {
+    checkFormValid,
+    checkIsValid,
+    validation,
+    disabled,
+    setDisabled,
+    resetValidation,
+  };
+};

+ 22 - 0
client/src/hooks/Pagination.ts

@@ -0,0 +1,22 @@
+import { useState } from 'react';
+
+const PAGE_SIZE = 15;
+export const usePaginationHook = () => {
+  const [offset, setOffset] = useState(0);
+  const [currentPage, setCurrentPage] = useState(0);
+  const [total, setTotal] = useState(0);
+
+  const handleCurrentPage = (page: number) => {
+    setCurrentPage(page);
+    setOffset(PAGE_SIZE * page);
+  };
+
+  return {
+    offset,
+    currentPage,
+    pageSize: PAGE_SIZE,
+    handleCurrentPage,
+    total,
+    setTotal,
+  };
+};

+ 29 - 0
client/src/hooks/Polling.ts

@@ -0,0 +1,29 @@
+import dayjs from 'dayjs';
+import { useEffect, useRef } from 'react';
+import { PollingTypeEnum } from '../consts/Polling';
+
+export const useInterval = (
+  callback: (isInterval?: boolean) => void,
+  delay: number | null,
+  type: PollingTypeEnum
+) => {
+  const savedCallback = useRef<(isInterval?: boolean) => void>(() => {});
+  useEffect(() => {
+    savedCallback.current = callback;
+  }, [callback]);
+
+  useEffect(() => {
+    const cb = () => {
+      console.log(
+        `starting fetch data for ${type}`,
+        dayjs().format('YYYY-MM-DD HH:mm:ss')
+      );
+      savedCallback.current(true);
+    };
+
+    if (delay !== null) {
+      let timer = setInterval(cb, delay);
+      return () => clearInterval(timer);
+    }
+  }, [delay, type]);
+};

+ 29 - 0
client/src/hooks/__test__/CommonStyle.spec.tsx

@@ -0,0 +1,29 @@
+import { render } from '@testing-library/react';
+
+import { usePageStyles } from '../CommonStyle';
+
+const setupUsePageStyles = () => {
+  const returnVal = {};
+
+  const TestComponent = () => {
+    Object.assign(returnVal, usePageStyles());
+    return null;
+  };
+
+  render(<TestComponent />);
+  return returnVal;
+};
+
+test('test usePageStyles', () => {
+  const style = setupUsePageStyles();
+  const expectedStyle = {
+    root: 'makeStyles-root-1',
+    titleWrapper: 'makeStyles-titleWrapper-2',
+    paper: 'makeStyles-paper-3',
+
+    titleContainer: 'makeStyles-titleContainer-4',
+    h2: 'makeStyles-h2-5',
+    tab: 'makeStyles-tab-6',
+  };
+  expect(style).toEqual(expectedStyle);
+});

+ 55 - 0
client/src/hooks/__test__/Form.spec.tsx

@@ -0,0 +1,55 @@
+import { render } from '@testing-library/react';
+import { IForm, useFormValidation } from '../Form';
+
+const mockForm: IForm[] = [
+  {
+    key: 'username',
+    value: '',
+    needCheck: true,
+  },
+  {
+    key: 'nickname',
+    value: '',
+    needCheck: false,
+  },
+];
+
+const setupUseFormValidation = () => {
+  const returnVal: any = {};
+
+  const TestComponent = () => {
+    Object.assign(returnVal, useFormValidation(mockForm));
+    return null;
+  };
+
+  render(<TestComponent />);
+  return returnVal;
+};
+
+test('test useFormValidation hook', () => {
+  const { checkFormValid, checkIsValid, validation } = setupUseFormValidation();
+
+  expect(checkFormValid(mockForm)).toBeFalsy();
+  expect(validation).toEqual([]);
+  expect(
+    checkIsValid({
+      value: '',
+      key: 'username',
+      rules: [{ rule: 'require', errorText: 'name is required' }],
+    }).result
+  ).toBeTruthy();
+  expect(
+    checkIsValid({
+      value: '11111',
+      key: 'email',
+      rules: [{ rule: 'email', errorText: 'email is invalid' }],
+    }).result
+  ).toBeTruthy();
+  expect(
+    checkIsValid({
+      value: '12345678aQ',
+      key: 'password',
+      rules: [{ rule: 'password', errorText: 'password is invalid' }],
+    }).result
+  ).toBeTruthy();
+});

+ 35 - 0
client/src/http/Axios.ts

@@ -0,0 +1,35 @@
+import axios from 'axios';
+import { SESSION } from '../consts/Localstorage';
+
+console.log(process.env.NODE_ENV, 'api:', process.env.REACT_APP_BASE_URL);
+console.log('docker env', (window as any)._env_, (window as any)._env_);
+
+export const url =
+  ((window as any)._env_ &&
+    (window as any)._env_.API_URL !== null &&
+    (window as any)._env_.API_URL !== 'null' &&
+    (window as any)._env_.API_URL) ||
+  process.env.REACT_APP_BASE_URL;
+
+const axiosInstance = axios.create({
+  baseURL: `${url}/api/v1`,
+  timeout: 10000,
+});
+
+axiosInstance.interceptors.request.use(
+  function (config) {
+    // Do something before request is sent
+    const session = window.localStorage.getItem(SESSION);
+
+    // console.log('in----', token);
+    session && (config.headers[SESSION] = session);
+
+    return config;
+  },
+  function (error) {
+    // Do something with request error
+    return Promise.reject(error);
+  }
+);
+
+export default axiosInstance;

+ 25 - 0
client/src/i18n/cn/button.ts

@@ -0,0 +1,25 @@
+export default {
+  cancel: '取消',
+  save: '保存',
+  reset: '重置',
+  update: '更新',
+  search: '搜索',
+  confirm: '确认',
+  connect: '连接',
+  import: '导入',
+  delete: '删除',
+  export: '导出',
+  verify: '验证邮件',
+  login: '登录',
+  resendEmail: '重发邮件',
+  createAccount: '创建账号',
+  signIn: '登录',
+  new: '新增',
+  signOut: '退出',
+  filter: '筛选',
+  print: '打印',
+  downloadCSV: '下载 csv 文件',
+  drop: 'Drop',
+  terminate: '停止',
+  send: '发送',
+};

+ 13 - 0
client/src/i18n/cn/card.ts

@@ -0,0 +1,13 @@
+export default {
+  // base card
+  core: 'cores',
+  memory: 'memory',
+  capacity: 'capacity',
+  count: 'node count',
+
+  // info card
+  dimension: 'Dimension',
+  metric: 'Metric type',
+  createTime: 'Created time',
+  index: 'Index',
+};

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

@@ -0,0 +1,31 @@
+export default {
+  // dialog
+  collection: 'Collection',
+  create: 'Create Collection',
+  edit: 'Edit Collection',
+  deleteMultipleTitle: 'Delete {{number}} Collections',
+  deleteWarning:
+    'You are trying to delete a collection with data. This action cannot be undone.',
+  deleteMultipleWarning:
+    'You are trying to delete multiple collections. This action cannot be undone.',
+
+  // dialog form and table
+  name: 'Name',
+  database: 'Database',
+  dimension: 'Dimension',
+  metric: 'Metric Type',
+  desc: 'Description',
+  time: 'Created Time',
+  partition: 'Partition Count',
+  index: 'Index count',
+  location: 'Location',
+
+  empty: 'No Collection',
+
+  // dialog snackbar
+  createSuccess: `"{{collectionName}}" is created in "{{dbName}}"`,
+  editSuccess: 'Collection is updated',
+
+  deleteMultipleSuccess: '{{number}} collections are deleted',
+  deleteSuccess: `Collection "{{name}}" is deleted`,
+};

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

@@ -0,0 +1,77 @@
+export default {
+  status: {
+    creating: 'creating',
+    running: 'running',
+    error: 'error',
+  },
+  grid: {
+    action: 'action',
+    noData: 'No Data',
+    rows: 'Rows',
+    of: 'of',
+    nextLabel: 'next page',
+    prevLabel: 'prev page',
+  },
+  nav: {
+    database: 'Database',
+    index: 'Index',
+    query: 'Query Service',
+    task: 'Task',
+
+    menu: {
+      database: 'Database',
+      collection: 'Collection',
+      partition: 'Partition',
+      index: 'Index',
+      query: 'Query',
+    },
+  },
+  copy: {
+    copy: 'Copy',
+    copied: 'Copied',
+  },
+  header: {
+    navigation: {
+      allDatabases: 'All Databases',
+      allTasks: 'Task',
+      allQueries: 'All Queries',
+    },
+  },
+  oauth: {
+    divider: 'OR',
+    signIn: {
+      google: 'Sign in with Google',
+      success: 'Sign in successfully',
+    },
+    signUp: {
+      google: 'Sign up with Google',
+    },
+  },
+  stepper: {
+    back: 'Back',
+    next: 'Next',
+  },
+  autoComplete: {
+    all: 'View All',
+    searchTitle: 'Search by category',
+    database: 'Database',
+    collection: 'Collection',
+    partition: 'Partition',
+    index: 'Index',
+    query: 'Query',
+  },
+  tabs: {
+    collection: 'Collections',
+    partitions: 'Partitions',
+    schema: 'Schema',
+    index: 'Index',
+    data: 'Data',
+  },
+  schema: {
+    name: 'Field Name',
+    type: 'Type',
+    dimension: 'Dimension',
+    desc: 'Descriptions',
+  },
+  uploadSuccess: 'Upload successfully',
+};

+ 12 - 0
client/src/i18n/cn/connect.ts

@@ -0,0 +1,12 @@
+export default {
+  connect: 'Connect',
+  title: 'Connect Your Service',
+  subtitle: 'Here are the fast way to connect your Zilliz Cloud',
+  download: 'Download Golang SDK',
+  tip: 'More operations, please check the documentation',
+  link: 'Go to Milvus Doc',
+
+  start: 'Quick Start',
+  py: 'Python',
+  go: 'Golang',
+};

+ 5 - 0
client/src/i18n/cn/data.ts

@@ -0,0 +1,5 @@
+export default {
+  id: 'ID',
+  vector: 'Vector',
+  empty: 'No Data',
+};

+ 43 - 0
client/src/i18n/cn/database.ts

@@ -0,0 +1,43 @@
+export default {
+  // dialog
+  db: 'Database',
+  empty: 'No Database',
+
+  create: 'Create Database',
+  editDb: 'Edit Database',
+  deleteDb: 'Delete Database',
+  deleteWarning:
+    'Deleting this database will terminate all related query services, this action can not be undone.',
+
+  // card
+  endpoint: 'Endpoint',
+  token: 'Access Token',
+  query: 'Query Service',
+  addQuery: 'Add Query Service',
+  write: 'Write Node',
+  // view: 'View Collections',
+  connect: 'Connect Your Service',
+  edit: 'edit',
+  delete: 'delete',
+  createCollection: 'Create Collection',
+  createQuery: 'Create Query Service',
+  createQueryTip: 'one database can only has one query service',
+
+  // dialog form
+  name: 'Name',
+  provider: 'Cloud Provider',
+  region: 'Cloud Region',
+  node: 'Node Type',
+  price: 'Price',
+  desc: 'Description',
+  time: 'Create Time',
+  status: 'Status',
+
+  // dialog snackbar
+  createSuccess: `Start creating Database "{{dbName}}"`,
+  editSuccess: `Database "{{name}}" is updated`,
+  deleteSuccess: `Database "{{name}}" is deleted`,
+
+  // tooltip
+  viewCollectionTip: 'Database is creating',
+};

+ 8 - 0
client/src/i18n/cn/dialog.ts

@@ -0,0 +1,8 @@
+export default {
+  deleteTipAction: 'Type',
+  deleteTipPurpose: 'to confirm.',
+  deleteTitle: `Delete {{type}} "{{name}}"`,
+  dropTitle: `Drop {{type}} {{name}}`,
+
+  createTitle: `Create {{type}} in "{{name}}"`,
+};

+ 15 - 0
client/src/i18n/cn/help.ts

@@ -0,0 +1,15 @@
+export default {
+  text: '发送反馈',
+  tip: '',
+  title: '联系我们',
+  content: '有任何意见或问题?',
+  content2: '请告诉我们.',
+  placeholder: '填写你的经历',
+  success: '您的反馈已发送成功',
+
+  // type
+  support: 'Support',
+  bug: 'Bug',
+  suggestion: 'Suggestion',
+  other: 'Other',
+};

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