Gitea 3 years ago
parent
commit
7f0e4c7341
56 changed files with 1035 additions and 340 deletions
  1. 5 3
      README.md
  2. 1 1
      client/.prettierrc
  3. 6 2
      client/package.json
  4. 4 0
      client/public/index.html
  5. 3 0
      client/src/assets/icons/people.svg
  6. 6 0
      client/src/assets/icons/slack.svg
  7. BIN
      client/src/assets/imgs/wechat_qrcode.png
  8. 0 38
      client/src/components/__test__/copy/Copy.spec.tsx
  9. 10 11
      client/src/components/advancedSearch/CopyButton.tsx
  10. 1 0
      client/src/components/advancedSearch/Types.ts
  11. 1 1
      client/src/components/advancedSearch/index.tsx
  12. 63 0
      client/src/components/code/CodeBlock.tsx
  13. 113 0
      client/src/components/code/CodeView.tsx
  14. 20 0
      client/src/components/code/Types.ts
  15. 0 55
      client/src/components/copy/Copy.tsx
  16. 27 7
      client/src/components/customButton/CustomIconButton.tsx
  17. 9 6
      client/src/components/customDialog/CustomDialog.tsx
  18. 1 4
      client/src/components/customDialog/CustomDialogTitle.tsx
  19. 1 0
      client/src/components/customDialog/DeleteDialogTemplate.tsx
  20. 78 28
      client/src/components/customDialog/DialogTemplate.tsx
  21. 4 0
      client/src/components/customDialog/Types.ts
  22. 1 1
      client/src/components/customProgress/CustomLinearProgress.tsx
  23. 33 0
      client/src/components/customSwitch/CustomSwitch.tsx
  24. 3 0
      client/src/components/customSwitch/Types.ts
  25. 4 2
      client/src/components/customTabList/CustomTabList.tsx
  26. 1 0
      client/src/components/customTabList/Types.ts
  27. 5 2
      client/src/components/grid/Grid.tsx
  28. 13 2
      client/src/components/grid/Table.tsx
  29. 4 2
      client/src/components/grid/TablePaginationActions.tsx
  30. 2 1
      client/src/components/grid/ToolBar.tsx
  31. 7 2
      client/src/components/insert/Container.tsx
  32. 2 2
      client/src/components/insert/Import.tsx
  33. 0 105
      client/src/components/layout/GlobalToolbar.tsx
  34. 2 2
      client/src/components/layout/Header.tsx
  35. 193 0
      client/src/components/menu/CommunityBtn.tsx
  36. 26 32
      client/src/components/menu/NavMenu.tsx
  37. 2 2
      client/src/components/status/Status.tsx
  38. 1 3
      client/src/context/Auth.tsx
  39. 15 1
      client/src/i18n/cn/common.ts
  40. 15 1
      client/src/i18n/en/common.ts
  41. 6 1
      client/src/index.css
  42. 1 1
      client/src/pages/connect/Connect.tsx
  43. 5 1
      client/src/pages/overview/collectionCard/CollectionCard.tsx
  44. 1 1
      client/src/pages/overview/statisticsCard/StatisticsCard.tsx
  45. 51 11
      client/src/pages/schema/Create.tsx
  46. 23 4
      client/src/pages/schema/IndexTypeElement.tsx
  47. 1 1
      client/src/pages/schema/Schema.tsx
  48. 27 3
      client/src/pages/seach/VectorSearch.tsx
  49. 14 0
      client/src/styles/common.css
  50. 5 0
      client/src/styles/theme.ts
  51. 14 0
      client/src/utils/Format.ts
  52. 16 0
      client/src/utils/code/Js.ts
  53. 25 0
      client/src/utils/code/Py.ts
  54. 6 0
      client/src/utils/code/Types.ts
  55. 1 1
      client/src/utils/search.ts
  56. 157 0
      client/yarn.lock

+ 5 - 3
README.md

@@ -1,4 +1,6 @@
 # Milvus insight
+[![typescript](https://badges.aleen42.com/src/typescript.svg)](https://badges.aleen42.com/src/typescript.svg)
+[![downloads](https://img.shields.io/docker/pulls/milvusdb/milvus-insight)](https://img.shields.io/docker/pulls/milvusdb/milvus-insight)
 
 Milvus insight provides an intuitive and efficient GUI for Milvus, allowing you to interact with your databases and manage your data with just few clicks.
 
@@ -41,7 +43,7 @@ Once you start the docker, open the browser, type `http://{ your machine IP }:80
 | HOST_URL   | http://192.168.0.1:8000 |   true   | Where Milvus insight container is installed |
 | MILVUS_URL | 192.168.0.1:19530       |  false   | Optional, Milvus server URL                 |
 
-Tip: **127.0.0.1 or localhost will not working when run by docker**
+Tip: **127.0.0.1 or localhost will not work when runs on docker**
 
 #### Try the dev build
 
@@ -60,14 +62,14 @@ out an open PR:
 
 1. Fork and clone the repo
 2. `cd server` go to the server directory
-3. `$ yarn install` to install dependencies
+3. `yarn install` to install dependencies
 4. Create a branch for your PR
 
 ### Build client
 
 1. Fork and clone the repo
 2. `cd client` go to the client directory
-3. `$ yarn install` to install dependencies
+3. `yarn install` to install dependencies
 4. Create a branch for your PR
 
 ### Milvus

+ 1 - 1
.prettierrc → client/.prettierrc

@@ -7,4 +7,4 @@
   "trailingComma": "es5",
   "bracketSpacing": true,
   "arrowParens": "avoid"
-}
+}

+ 6 - 2
client/package.json

@@ -19,6 +19,7 @@
     "@types/react-dom": "^17.0.0",
     "@types/react-highlight-words": "^0.16.2",
     "@types/react-router-dom": "^5.1.7",
+    "@types/react-syntax-highlighter": "^13.5.2",
     "axios": "^0.21.1",
     "dayjs": "^1.10.5",
     "i18next": "^20.3.1",
@@ -30,6 +31,7 @@
     "react-i18next": "^11.10.0",
     "react-router-dom": "^5.2.0",
     "react-scripts": "4.0.3",
+    "react-syntax-highlighter": "^15.4.4",
     "socket.io-client": "^4.1.3",
     "typescript": "^4.1.2",
     "web-vitals": "^1.0.1"
@@ -44,7 +46,8 @@
     "test:watch": "react-app-rewired test --watch",
     "test:cov": "react-app-rewired test --watchAll=false --coverage",
     "test:report": "react-app-rewired test --watchAll=false --coverage --coverageReporters='text-summary'",
-    "eject": "react-app-rewired eject"
+    "eject": "react-app-rewired eject",
+    "format": "prettier --write '**/*.{ts,js,tsx,jsx,css}'"
   },
   "eslintConfig": {
     "extends": [
@@ -65,6 +68,7 @@
     ]
   },
   "devDependencies": {
-    "@testing-library/react-hooks": "^7.0.1"
+    "@testing-library/react-hooks": "^7.0.1",
+    "prettier": "2.3.2"
   }
 }

+ 4 - 0
client/public/index.html

@@ -19,6 +19,10 @@
       href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap"
       rel="stylesheet"
     />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@300;400&display=swap"
+      rel="stylesheet"
+    />
     <script src="%PUBLIC_URL%/env-config.js"></script>
 
     <!--

+ 3 - 0
client/src/assets/icons/people.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16 11C17.66 11 18.99 9.66 18.99 8C18.99 6.34 17.66 5 16 5C14.34 5 13 6.34 13 8C13 9.66 14.34 11 16 11ZM8 11C9.66 11 10.99 9.66 10.99 8C10.99 6.34 9.66 5 8 5C6.34 5 5 6.34 5 8C5 9.66 6.34 11 8 11ZM8 13C5.67 13 1 14.17 1 16.5V19H15V16.5C15 14.17 10.33 13 8 13ZM16 13C15.71 13 15.38 13.02 15.03 13.05C16.19 13.89 17 15.02 17 16.5V19H23V16.5C23 14.17 18.33 13 16 13Z" fill="#06AFF2"/>
+</svg>

+ 6 - 0
client/src/assets/icons/slack.svg

@@ -0,0 +1,6 @@
+<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.82519 0.0136337C6.72305 0.0136337 5.82252 0.914161 5.82252 2.0163C5.82252 3.11844 6.72305 4.01896 7.82519 4.01896H9.82786V2.0163C9.82786 0.914161 8.94077 0.0136337 7.82519 0.0136337ZM7.82519 5.3496H2.50267C1.40053 5.3496 0.5 6.25013 0.5 7.35226C0.5 8.4544 1.40053 9.35493 2.50267 9.35493H7.83863C8.94077 9.35493 9.8413 8.4544 9.8413 7.35226C9.82786 6.25013 8.94077 5.3496 7.82519 5.3496Z" fill="#36C5F0"/>
+<path d="M20.5 7.35207C20.5 6.24994 19.5995 5.34941 18.4973 5.34941C17.3952 5.34941 16.4947 6.24994 16.4947 7.35207V9.35474H18.4973C19.5995 9.36818 20.5 8.46765 20.5 7.35207ZM15.164 7.35207V2.00267C15.164 0.900527 14.2635 0 13.1614 0C12.0592 0 11.1587 0.900527 11.1587 2.00267V7.35207C11.1587 8.45421 12.0592 9.35474 13.1614 9.35474C14.2769 9.36818 15.164 8.46765 15.164 7.35207Z" fill="#2EB67D"/>
+<path d="M13.1614 20.0536C14.2635 20.0536 15.164 19.1531 15.164 18.051C15.164 16.9488 14.2635 16.0483 13.1614 16.0483H11.1587V18.051C11.1587 19.1531 12.0592 20.0536 13.1614 20.0536ZM13.1614 14.7042H18.4973C19.5995 14.7042 20.5 13.8037 20.5 12.7016C20.5 11.5994 19.5995 10.6989 18.4973 10.6989H13.1614C12.0592 10.6989 11.1587 11.5994 11.1587 12.7016C11.1587 13.8037 12.0592 14.7042 13.1614 14.7042Z" fill="#ECB22E"/>
+<path d="M0.5 12.7016C0.5 13.8037 1.40053 14.7042 2.50267 14.7042C3.60481 14.7042 4.50533 13.8037 4.50533 12.7016V10.6989H2.50267C1.38709 10.6989 0.5 11.5994 0.5 12.7016ZM5.83597 12.7016V18.051C5.83597 19.1531 6.73649 20.0536 7.83863 20.0536C8.94077 20.0536 9.8413 19.1531 9.8413 18.051V12.7016C9.8413 11.5994 8.94077 10.6989 7.83863 10.6989C6.72305 10.6989 5.83597 11.5994 5.83597 12.7016Z" fill="#E01E5A"/>
+</svg>

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


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

@@ -1,38 +0,0 @@
-import { ReactNode } from 'react';
-import { render, unmountComponentAtNode } from 'react-dom';
-import { act } from 'react-dom/test-utils';
-import Copy from '../../copy/Copy';
-let container: any = null;
-
-jest.mock('react-i18next', () => ({
-  useTranslation: () => ({
-    t: (key: any) => key,
-  }),
-}));
-
-jest.mock('@material-ui/core/Tooltip', () => {
-  return (props: { children: ReactNode }) => {
-    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);
-  });
-});

+ 10 - 11
client/src/components/advancedSearch/CopyButton.tsx

@@ -3,25 +3,24 @@ import { makeStyles, Theme, createStyles } from '@material-ui/core';
 import { CopyButtonProps } from './Types';
 import icons from '../icons/Icons';
 import CustomIconButton from '../customButton/CustomIconButton';
+import { useTranslation } from 'react-i18next';
 
 const CopyIcon = icons.copyExpression;
 
 const CopyButton: FC<CopyButtonProps> = props => {
-  const {
-    label = 'copy button',
-    icon,
-    className,
-    value = '',
-    ...others
-  } = props;
+  const { label, icon, className, value = '', ...others } = props;
   const classes = useStyles();
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
   const [tooltipTitle, setTooltipTitle] = useState('Copy');
 
-  const handleClick = (v: string) => {
-    setTooltipTitle('Copied!');
+  const handleClick = (event: React.MouseEvent<HTMLElement>, v: string) => {
+    event.stopPropagation();
+
+    setTooltipTitle(copyTrans.copied);
     navigator.clipboard.writeText(v);
     setTimeout(() => {
-      setTooltipTitle('Copy');
+      setTooltipTitle(copyTrans.copy);
     }, 1000);
   };
 
@@ -30,7 +29,7 @@ const CopyButton: FC<CopyButtonProps> = props => {
       tooltip={tooltipTitle}
       aria-label={label}
       className={`${classes.button} ${className}`}
-      onClick={() => handleClick(value || '')}
+      onClick={event => handleClick(event, value || '')}
       {...others}
     >
       {icon || <CopyIcon style={{ color: 'transparent' }} />}

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

@@ -44,6 +44,7 @@ export interface AddConditionProps {
 export interface CopyButtonProps {
   className?: string;
   icon?: any;
+  // needed for accessibility, will not show on page
   label: string;
   value: string;
   others?: any;

+ 1 - 1
client/src/components/advancedSearch/index.tsx

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

+ 63 - 0
client/src/components/code/CodeBlock.tsx

@@ -0,0 +1,63 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import CopyButton from '../advancedSearch/CopyButton';
+import SyntaxHighlighter from 'react-syntax-highlighter';
+import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
+import { FC } from 'react';
+import { CodeBlockProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    position: 'relative',
+    padding: theme.spacing(3),
+    borderRadius: 8,
+    backgroundColor: '#fff',
+    color: '#454545',
+  },
+  block: {
+    margin: 0,
+  },
+  copy: {
+    position: 'absolute',
+    top: theme.spacing(2),
+    right: theme.spacing(2),
+  },
+}));
+
+const CodeStyle = {
+  backgroundColor: '#fff',
+  padding: 0,
+  margin: 0,
+  marginRight: 32,
+  fontSize: 14,
+};
+
+const CodeBlock: FC<CodeBlockProps> = ({
+  code,
+  language,
+  wrapperClass = '',
+}) => {
+  const classes = getStyles();
+
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
+
+  return (
+    <div className={`${classes.wrapper} ${wrapperClass}`}>
+      <CopyButton
+        className={classes.copy}
+        label={copyTrans.label}
+        value={code}
+      />
+      <SyntaxHighlighter
+        language={language}
+        style={docco}
+        customStyle={CodeStyle}
+      >
+        {code}
+      </SyntaxHighlighter>
+    </div>
+  );
+};
+
+export default CodeBlock;

+ 113 - 0
client/src/components/code/CodeView.tsx

@@ -0,0 +1,113 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import CustomTabList from '../customTabList/CustomTabList';
+import { ITab } from '../customTabList/Types';
+import CodeBlock from './CodeBlock';
+import { CodeViewProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    boxSizing: 'border-box',
+    width: '100%',
+
+    padding: theme.spacing(4),
+    backgroundColor: theme.palette.milvusDark.main,
+    borderRadius: 8,
+
+    color: '#fff',
+  },
+  title: {
+    marginBottom: theme.spacing(2),
+  },
+
+  // override tab list style
+  tabs: {
+    minHeight: 0,
+
+    '& .MuiTab-wrapper': {
+      textTransform: 'uppercase',
+      fontWeight: 'bold',
+      color: '#fff',
+    },
+
+    '& .MuiTab-root': {
+      minHeight: 18,
+      marginRight: 0,
+    },
+
+    // disable Ripple Effect
+    '& .MuiTouchRipple-root': {
+      display: 'none',
+    },
+
+    '& .Mui-selected': {
+      '& .MuiTab-wrapper': {
+        color: theme.palette.primary.main,
+      },
+    },
+
+    '& .MuiTabs-indicator': {
+      display: 'flex',
+      justifyContent: 'center',
+
+      top: 32,
+      backgroundColor: 'transparent',
+
+      '& .tab-indicator': {
+        height: 1,
+        width: '100%',
+        maxWidth: 26,
+        backgroundColor: theme.palette.primary.main,
+      },
+    },
+
+    '& .MuiTabs-flexContainer': {
+      borderBottom: 'none',
+    },
+  },
+
+  block: {
+    /**
+     * container height minus:
+     * 1. CodeView padding top and bottom (32 * 2)
+     * 2. CodeBlock padding top and bottom (24 * 2)
+     * 3. title height and margin bottom (24 + 16)
+     * 4. tab title height and margin bottom (36 + 16)
+     */
+    height: (props: { height: number }) =>
+      props.height - 32 * 2 - 24 * 2 - (24 + 16) - (36 + 16),
+    overflowY: 'auto',
+  },
+}));
+
+const CodeView: FC<CodeViewProps> = ({
+  wrapperClass = '',
+  data,
+  height = 0,
+}) => {
+  const classes = getStyles({ height });
+  const { t: commonTrans } = useTranslation();
+
+  const tabs: ITab[] = data.map(item => ({
+    label: item.label,
+    component: (
+      <CodeBlock
+        wrapperClass={height !== 0 ? classes.block : ''}
+        language={item.language}
+        code={item.code}
+      />
+    ),
+  }));
+
+  return (
+    <section className={`${classes.wrapper} ${wrapperClass}`}>
+      <Typography variant="h5" className={classes.title}>
+        {commonTrans('code')}
+      </Typography>
+      <CustomTabList tabs={tabs} wrapperClass={classes.tabs} />
+    </section>
+  );
+};
+
+export default CodeView;

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

@@ -0,0 +1,20 @@
+export interface CodeViewProps {
+  height?: number;
+  wrapperClass?: string;
+  data: CodeViewData[];
+}
+
+export enum CodeLanguageEnum {
+  javascript = 'javascript',
+  python = 'python',
+}
+
+export interface CodeBlockProps {
+  code: string;
+  language: CodeLanguageEnum;
+  wrapperClass?: string;
+}
+
+export interface CodeViewData extends CodeBlockProps {
+  label: string;
+}

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

@@ -1,55 +0,0 @@
-import { IconButton, makeStyles } from '@material-ui/core';
-import { useState } from 'react';
-import React 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;

+ 27 - 7
client/src/components/customButton/CustomIconButton.tsx

@@ -1,20 +1,40 @@
-import { IconButtonProps, Tooltip, IconButton } from '@material-ui/core';
+import {
+  IconButtonProps,
+  Tooltip,
+  IconButton,
+  makeStyles,
+  Theme,
+} from '@material-ui/core';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'inline-block',
+  },
+  iconBtn: {
+    padding: theme.spacing(1),
+  },
+}));
 
 const CustomIconButton = (props: IconButtonProps & { tooltip?: string }) => {
-  const { tooltip, ...otherProps } = props;
+  const { tooltip, className, ...otherProps } = props;
+  const classes = getStyles();
 
   return (
-    <>
+    <div className={`${classes.wrapper} ${className}`}>
       {tooltip ? (
-        <Tooltip title={tooltip}>
+        <Tooltip title={tooltip} arrow>
           <span>
-            <IconButton {...otherProps}>{props.children}</IconButton>
+            <IconButton classes={{ root: classes.iconBtn }} {...otherProps}>
+              {props.children}
+            </IconButton>
           </span>
         </Tooltip>
       ) : (
-        <IconButton {...otherProps}>{props.children}</IconButton>
+        <IconButton classes={{ root: classes.iconBtn }} {...otherProps}>
+          {props.children}
+        </IconButton>
       )}
-    </>
+    </div>
   );
 };
 

+ 9 - 6
client/src/components/customDialog/CustomDialog.tsx

@@ -16,12 +16,15 @@ import CustomDialogTitle from './CustomDialogTitle';
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
     paper: {
-      minWidth: '480px',
-      borderRadius: '8px',
+      minWidth: 480,
+      borderRadius: 8,
       padding: 0,
+
+      backgroundColor: 'transparent',
     },
     noticePaper: {
-      maxWidth: '480px',
+      backgroundColor: '#fff',
+      maxWidth: 480,
     },
     paperSm: {
       maxWidth: '80%',
@@ -31,12 +34,12 @@ const useStyles = makeStyles((theme: Theme) =>
     },
     title: {
       '& p': {
-        fontWeight: '500',
+        fontWeight: 500,
         overflow: 'hidden',
         textOverflow: 'ellipsis',
         whiteSpace: 'nowrap',
-        maxWidth: '300px',
-        fontSize: '20px',
+        maxWidth: 300,
+        fontSize: 20,
       },
     },
     padding: {

+ 1 - 4
client/src/components/customDialog/CustomDialogTitle.tsx

@@ -14,15 +14,12 @@ const getStyles = makeStyles((theme: Theme) => ({
     justifyContent: 'space-between',
     alignItems: 'center',
   },
-  // closeButton: {
-  //   padding: theme.spacing(1),
-  // },
   title: {
     fontWeight: 500,
   },
   icon: {
     fontSize: '24px',
-    color: '#010e29',
+    color: theme.palette.milvusDark.main,
 
     cursor: 'pointer',
   },

+ 1 - 0
client/src/components/customDialog/DeleteDialogTemplate.tsx

@@ -16,6 +16,7 @@ import { rootContext } from '../../context/Root';
 const useStyles = makeStyles((theme: Theme) => ({
   root: {
     maxWidth: '480px',
+    backgroundColor: '#fff',
   },
   mb: {
     marginBottom: theme.spacing(2.5),

+ 78 - 28
client/src/components/customDialog/DialogTemplate.tsx

@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { FC, useEffect, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import {
   DialogContent,
@@ -9,8 +9,29 @@ import {
 import { DialogContainerProps } from './Types';
 import CustomDialogTitle from './CustomDialogTitle';
 import CustomButton from '../customButton/CustomButton';
+import CodeView from '../code/CodeView';
 
 const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+  },
+  block: {
+    borderRadius: 8,
+    backgroundColor: '#fff',
+  },
+  dialog: {
+    minWidth: 480,
+  },
+  codeWrapper: {
+    width: (props: { showCode: boolean }) => (props.showCode ? 480 : 0),
+    transition: 'width 0.2s',
+  },
+  code: {
+    height: '100%',
+    // set code view padding 0 if not show
+    padding: (props: { showCode: boolean }) =>
+      props.showCode ? theme.spacing(4) : 0,
+  },
   actions: {
     paddingTop: theme.spacing(2),
     justifyContent: 'space-between',
@@ -30,41 +51,70 @@ const DialogTemplate: FC<DialogContainerProps> = ({
   showCancel = true,
   showCloseIcon = true,
   leftActions,
+  // needed for code mode
+  showCode = false,
+  codeBlocksData = [],
 }) => {
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
   const confirm = confirmLabel || t('confirm');
-  const classes = useStyles();
+  const classes = useStyles({ showCode });
   const onCancel = handleCancel || handleClose;
 
+  const dialogRef = useRef(null);
+  const [dialogHeight, setDialogHeight] = useState<number>(0);
+
+  /**
+   * code mode height should not over original dialog height
+   * everytime children change, should recalculate dialog height
+   */
+  useEffect(() => {
+    if (dialogRef.current) {
+      const height = (dialogRef.current as any).offsetHeight;
+      setDialogHeight(height);
+    }
+  }, [children]);
+
   return (
-    <>
-      <CustomDialogTitle onClose={handleClose} showCloseIcon={showCloseIcon}>
-        {title}
-      </CustomDialogTitle>
-      <DialogContent>{children}</DialogContent>
-      {showActions && (
-        <DialogActions className={classes.actions}>
-          <div>{leftActions}</div>
-          <div>
-            {showCancel && (
-              <CustomButton onClick={onCancel} color="default" name="cancel">
-                {cancel}
+    <section className={classes.wrapper}>
+      <div ref={dialogRef} className={`${classes.dialog} ${classes.block}`}>
+        <CustomDialogTitle onClose={handleClose} showCloseIcon={showCloseIcon}>
+          {title}
+        </CustomDialogTitle>
+        <DialogContent>{children}</DialogContent>
+        {showActions && (
+          <DialogActions className={classes.actions}>
+            <div>{leftActions}</div>
+            <div>
+              {showCancel && (
+                <CustomButton onClick={onCancel} color="default" name="cancel">
+                  {cancel}
+                </CustomButton>
+              )}
+              <CustomButton
+                variant="contained"
+                onClick={handleConfirm}
+                color="primary"
+                disabled={confirmDisabled}
+                name="confirm"
+              >
+                {confirm}
               </CustomButton>
-            )}
-            <CustomButton
-              variant="contained"
-              onClick={handleConfirm}
-              color="primary"
-              disabled={confirmDisabled}
-              name="confirm"
-            >
-              {confirm}
-            </CustomButton>
-          </div>
-        </DialogActions>
-      )}
-    </>
+            </div>
+          </DialogActions>
+        )}
+      </div>
+
+      <div className={`${classes.block} ${classes.codeWrapper}`}>
+        {showCode && (
+          <CodeView
+            height={dialogHeight}
+            wrapperClass={classes.code}
+            data={codeBlocksData}
+          />
+        )}
+      </div>
+    </section>
   );
 };
 

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

@@ -1,5 +1,6 @@
 import { ReactElement } from 'react';
 import { DialogType } from '../../context/Types';
+import { CodeViewData } from '../code/Types';
 export type CustomDialogType = DialogType & {
   onClose: () => void;
   containerClass?: string;
@@ -32,4 +33,7 @@ export type DialogContainerProps = {
   showActions?: boolean;
   showCancel?: boolean;
   leftActions?: ReactElement;
+  // code mode requirement
+  showCode?: boolean;
+  codeBlocksData?: CodeViewData[];
 };

+ 1 - 1
client/src/components/customProgress/CustomLinearProgress.tsx

@@ -17,7 +17,7 @@ const getProgressStyles = makeStyles((theme: Theme) => ({
   percent: {
     minWidth: '35px',
     marginLeft: theme.spacing(1),
-    color: '#010e29',
+    color: theme.palette.milvusDark.main,
   },
 }));
 

+ 33 - 0
client/src/components/customSwitch/CustomSwitch.tsx

@@ -0,0 +1,33 @@
+import { FormControlLabel, makeStyles, Switch, Theme } from '@material-ui/core';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { CustomSwitchProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  label: {
+    color: '#757575',
+  },
+
+  placement: {
+    marginLeft: 0,
+  },
+}));
+
+const CustomSwitch: FC<CustomSwitchProps> = ({ onChange }) => {
+  const classes = getStyles();
+  const { t: commonTrans } = useTranslation();
+
+  return (
+    <FormControlLabel
+      classes={{
+        label: classes.label,
+        labelPlacementStart: classes.placement,
+      }}
+      label={commonTrans('view')}
+      labelPlacement="start"
+      control={<Switch color="primary" onChange={onChange} />}
+    />
+  );
+};
+
+export default CustomSwitch;

+ 3 - 0
client/src/components/customSwitch/Types.ts

@@ -0,0 +1,3 @@
+export interface CustomSwitchProps {
+  onChange: (event: React.ChangeEvent<{ checked: boolean }>) => void;
+}

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

@@ -53,7 +53,7 @@ const a11yProps = (index: number) => {
 };
 
 const CustomTabList: FC<ITabListProps> = props => {
-  const { tabs, activeIndex = 0, handleTabChange } = props;
+  const { tabs, activeIndex = 0, handleTabChange, wrapperClass = '' } = props;
   const classes = useStyles();
   const [value, setValue] = useState<number>(activeIndex);
 
@@ -67,10 +67,12 @@ const CustomTabList: FC<ITabListProps> = props => {
     <>
       <Tabs
         classes={{
-          root: classes.wrapper,
+          root: `${classes.wrapper} ${wrapperClass}`,
           indicator: classes.tab,
           flexContainer: classes.tabContainer,
         }}
+        // if not provide this property, Material will add single span element by default
+        TabIndicatorProps={{ children: <div className="tab-indicator" /> }}
         value={value}
         onChange={handleChange}
         aria-label="tabs"

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

@@ -9,6 +9,7 @@ export interface ITabListProps {
   tabs: ITab[];
   activeIndex?: number;
   handleTabChange?: (index: number) => void;
+  wrapperClass?: string;
 }
 
 export interface ITabPanel {

+ 5 - 2
client/src/components/grid/Grid.tsx

@@ -73,8 +73,11 @@ const userStyle = makeStyles(theme => ({
 
 const MilvusGrid: FC<MilvusGridType> = props => {
   const classes = userStyle();
-  const { t } = useTranslation();
-  const gridTrans = t('grid') as any;
+
+  // i18n
+  const { t: commonTrans } = useTranslation();
+  const gridTrans = commonTrans('grid');
+
   const {
     rowCount = 10,
     rowsPerPage = 5,

+ 13 - 2
client/src/components/grid/Table.tsx

@@ -12,9 +12,10 @@ import { Box, Button, Typography } from '@material-ui/core';
 import EnhancedTableHead from './TableHead';
 import EditableTableHead from './TableEditableHead';
 import { stableSort, getComparator } from './Utils';
-import Copy from '../../components/copy/Copy';
 import ActionBar from './ActionBar';
 import LoadingTable from './LoadingTable';
+import CopyButton from '../advancedSearch/CopyButton';
+import { useTranslation } from 'react-i18next';
 
 const useStyles = makeStyles(theme => ({
   root: {
@@ -99,6 +100,9 @@ const useStyles = makeStyles(theme => ({
     letterSpacing: '0.5px',
     color: 'rgba(0, 0, 0, 0.6)',
   },
+  copyBtn: {
+    marginLeft: theme.spacing(0.5),
+  },
 }));
 
 const EnhancedTable: FC<TableType> = props => {
@@ -134,6 +138,9 @@ const EnhancedTable: FC<TableType> = props => {
 
   const containerRef = useRef(null);
 
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
+
   const handleRequestSort = (event: any, property: string) => {
     const isAsc = orderBy === property && order === 'asc';
     setOrder(isAsc ? 'desc' : 'asc');
@@ -313,7 +320,11 @@ const EnhancedTable: FC<TableType> = props => {
                               )}
 
                               {needCopy && row[colDef.id] && (
-                                <Copy data={row[colDef.id]} />
+                                <CopyButton
+                                  label={copyTrans.label}
+                                  value={row[colDef.id]}
+                                  className={classes.copyBtn}
+                                />
                               )}
                             </TableCell>
                           );

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

@@ -40,8 +40,10 @@ const useStyles = makeStyles((theme: Theme) =>
 const TablePaginationActions = (props: TablePaginationActionsProps) => {
   const classes = useStyles();
   const { count, page, rowsPerPage, onChangePage } = props;
-  const { t } = useTranslation();
-  const gridTrans = t('grid') as any;
+
+  // i18n
+  const { t: commonTrans } = useTranslation();
+  const gridTrans = commonTrans('grid');
 
   const handleBackButtonClick = (
     event: React.MouseEvent<HTMLButtonElement>

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

@@ -70,7 +70,8 @@ const CustomToolBar: FC<ToolBarType> = props => {
             const Icon = c.icon ? Icons[c.icon!]() : '';
             const disabled = c.disabled ? c.disabled(selected) : false;
             // when disabled "disabledTooltip" will replace "tooltip"
-            const tooltip = disabled && c.disabledTooltip ? c.disabledTooltip : c.tooltip;
+            const tooltip =
+              disabled && c.disabledTooltip ? c.disabledTooltip : c.tooltip;
             const isIcon = c.type === 'iconBtn';
 
             const btn = (

+ 7 - 2
client/src/components/insert/Container.tsx

@@ -116,6 +116,7 @@ const InsertContainer: FC<InsertContentProps> = ({
     setTableHeads(heads);
   }, [previewData, isContainFieldNames]);
 
+  // every time selected collection value change, partition options and default value will change
   const fetchPartition = useCallback(async () => {
     if (collectionValue) {
       const partitions = await PartitionHttp.getPartitions(collectionValue);
@@ -124,6 +125,12 @@ const InsertContainer: FC<InsertContentProps> = ({
         value: p._name,
       }));
       setPartitionOptions(partitionOptions);
+
+      if (partitionOptions.length > 0) {
+        // set first partition option value as default value
+        const [{ value: defaultPartitionValue }] = partitionOptions;
+        setPartitionValue(defaultPartitionValue as string);
+      }
     }
   }, [collectionValue]);
 
@@ -310,8 +317,6 @@ const InsertContainer: FC<InsertContentProps> = ({
 
   const handleCollectionChange = (name: string) => {
     setCollectionValue(name);
-    // reset partition
-    setPartitionValue('');
   };
 
   const handleNext = () => {

+ 2 - 2
client/src/components/insert/Import.tsx

@@ -24,7 +24,7 @@ const getStyles = makeStyles((theme: Theme) => ({
       '& .selectLabel': {
         fontSize: '14px',
         lineHeight: '20px',
-        color: '#010e29',
+        color: theme.palette.milvusDark.main,
       },
 
       '& .divider': {
@@ -187,7 +187,7 @@ const InsertImport: FC<InsertImportProps> = ({
           {insertTrans('noteTitle')}
         </Typography>
         <ul className="noteList">
-          {insertTrans('notes', { returnObjects: true }).map(note => (
+          {insertTrans('notes').map(note => (
             <li key={note} className="text noteItem">
               <Typography>{note}</Typography>
             </li>

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

@@ -1,105 +0,0 @@
-import { 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;

+ 2 - 2
client/src/components/layout/Header.tsx

@@ -61,8 +61,8 @@ const Header: FC<HeaderType> = props => {
   const { navInfo } = useContext(navContext);
   const { address, setAddress } = useContext(authContext);
   const history = useHistory();
-  const { t } = useTranslation();
-  const statusTrans: { [key in string]: string } = t('status');
+  const { t: commonTrans } = useTranslation();
+  const statusTrans = commonTrans('status');
   const BackIcon = icons.back;
   const LogoutIcon = icons.logout;
 

+ 193 - 0
client/src/components/menu/CommunityBtn.tsx

@@ -0,0 +1,193 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import Button from '@material-ui/core/Button';
+import SvgIcon from '@material-ui/core/SvgIcon';
+import { makeStyles, Theme, Link } from '@material-ui/core';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import GitHubIcon from '@material-ui/icons/GitHub';
+import { ReactComponent as peopleIcon } from '../../assets/icons/people.svg';
+import { ReactComponent as slackIcon } from '../../assets/icons/slack.svg';
+import qrcodePath from '../../assets/imgs/wechat_qrcode.png';
+
+const SLACK_LINK = 'https://slack.milvus.io';
+const GITHUB_LINK = 'https://github.com/milvus-io/milvus/discussions';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  root: {
+    bottom: theme.spacing(2),
+    position: 'absolute',
+    right: theme.spacing(3),
+    width: theme.spacing(5),
+    zIndex: 1,
+  },
+  menuBtn: {
+    border: '1px solid #E9E9ED',
+    borderRadius: '50%',
+    bottom: 0,
+    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+    height: theme.spacing(5),
+    minWidth: 'auto',
+    padding: 0,
+    position: 'absolute',
+    width: theme.spacing(5),
+  },
+  chevronIcon: {
+    transform: 'rotateZ(90deg)',
+    fill: theme.palette.primary.main,
+  },
+  container: {
+    bottom: theme.spacing(7),
+    position: 'absolute',
+    width: '360px',
+    overflow: 'hidden',
+    fontFamily: 'Roboto',
+  },
+  head: {
+    backgroundColor: theme.palette.primary.main,
+    padding: '16px 24px',
+    color: '#fff',
+    borderTopLeftRadius: theme.spacing(1),
+    borderTopRightRadius: theme.spacing(1),
+  },
+  title: {
+    fontWeight: 700,
+    fontSize: theme.spacing(2),
+    lineHeight: theme.spacing(3),
+    letterSpacing: '-0.01em',
+    fontFamily: 'Roboto'
+  },
+  titleDesc: {
+    color: '#f0f4f9',
+    fontSize: theme.spacing(1.5),
+    lineHeight: theme.spacing(2),
+  },
+  body: {
+    backgroundColor: '#fff',
+    border: '1px solid #e9e9e9',
+    borderTop: 0,
+    borderBottomRightRadius: theme.spacing(1),
+    borderBottomLeftRadius: theme.spacing(1),
+    padding: theme.spacing(3),
+  },
+  block: {
+    border: '1px solid #f9f9f9',
+    borderRadius: theme.spacing(1),
+    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+    marginBottom: theme.spacing(3),
+    padding: theme.spacing(2),
+  },
+  contentTitle: {
+    fontWeight: 500,
+    fontSize: theme.spacing(1.75),
+    lineHeight: theme.spacing(2.5),
+
+  },
+  contentDesc: {
+    fontSize: theme.spacing(1.5),
+    lineHeight: theme.spacing(2.5),
+    color: '#82838e',
+    marginBottom: theme.spacing(1),
+
+  },
+  contentLink: {
+    display: 'block',
+    fontSize: theme.spacing(1.5),
+    lineHeight: theme.spacing(2.5),
+    letterSpacing: '-0.01em',
+    color: theme.palette.primary.main,
+  },
+  qrImg: {
+    display: 'block',
+    margin: '0 auto',
+    width: theme.spacing(10),
+  },
+  textCenter: {
+    textAlign: 'center',
+  },
+  icon: {
+    marginTop: theme.spacing(2),
+    width: theme.spacing(2.5),
+    height: theme.spacing(2.5),
+  }
+}));
+
+const CommunityBtn = (props: any) => {
+  const [open, setOpen] = React.useState<boolean>(false);
+  const classes = getStyles();
+  const { t } = useTranslation();
+  const communityTrans: { [key in string]: string } = t('community');
+
+  return (
+    <div className={classes.root}>
+      {open && (
+        <div className={classes.container}>
+          <div className={classes.head}>
+            <div className={classes.title}>
+              {communityTrans.hi}
+            </div>
+            <div className={classes.titleDesc}>
+              {communityTrans.growing}
+            </div>
+          </div>
+          <div className={classes.body}>
+            <div className={classes.block}>
+              <div className={`${classes.contentTitle} ${classes.textCenter}`}>
+                {communityTrans.question}
+              </div>
+              <div className={`${classes.contentDesc} ${classes.textCenter}`}>
+                {communityTrans.qr}
+              </div>
+              <img className={classes.qrImg} src={qrcodePath} alt="qrcode" />
+            </div>
+            <div className={classes.block}>
+              <div className={`${classes.contentTitle} ${classes.textCenter}`}>
+                {communityTrans.more}
+              </div>
+
+              <SvgIcon viewBox="0 0 24 24" component={slackIcon} className={classes.icon} />
+              <div className={classes.contentDesc}>
+                {communityTrans.join}
+              </div>
+              <Link
+                classes={{ root: classes.contentLink }}
+                href={SLACK_LINK}
+                underline='always'
+                target="_blank"
+                rel="noopener"
+              >
+                {SLACK_LINK}
+              </Link>
+
+              <SvgIcon viewBox="0 0 24 24" component={GitHubIcon} className={classes.icon} />
+              <div className={classes.contentDesc}>
+                {communityTrans.get}
+              </div>
+              <Link
+                classes={{ root: classes.contentLink }}
+                href={GITHUB_LINK}
+                underline='always'
+                target="_blank"
+                rel="noopener"
+              >
+                {GITHUB_LINK}
+              </Link>
+            </div>
+          </div>
+        </div>
+      )}
+      <Button
+        className={classes.menuBtn}
+        aria-haspopup="true"
+        onClick={() => { setOpen(!open) }}
+      >
+        {open ?
+          <ChevronRightIcon className={classes.chevronIcon} />
+          :
+          <SvgIcon viewBox="0 0 24 24" component={peopleIcon} />
+        }
+      </Button>
+    </div>
+  );
+};
+
+export default CommunityBtn;

+ 26 - 32
client/src/components/menu/NavMenu.tsx

@@ -12,6 +12,7 @@ import icons from '../icons/Icons';
 import { useTranslation } from 'react-i18next';
 import Typography from '@material-ui/core/Typography';
 import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import CommunityBtn from './CommunityBtn';
 
 const timeout = 150;
 const duration = `${timeout}ms`;
@@ -28,7 +29,6 @@ const useStyles = makeStyles((theme: Theme) =>
       transition: theme.transitions.create('width', {
         duration,
       }),
-      overflow: 'hidden',
     },
     rootCollapse: {
       width: '86px',
@@ -74,7 +74,6 @@ const useStyles = makeStyles((theme: Theme) =>
     },
 
     logoWrapper: {
-      width: '100%',
       display: 'flex',
       alignItems: 'center',
       height: '86px',
@@ -139,7 +138,6 @@ const useStyles = makeStyles((theme: Theme) =>
     collapseIcon: {
       left: '73px',
     },
-
   })
 );
 
@@ -149,8 +147,8 @@ const NavMenu: FC<NavMenuType> = props => {
   const [expanded, setExpanded] = useState<boolean>(false);
   const [active, setActive] = useState<string>(defaultActive);
 
-  const { t } = useTranslation();
-  const milvusTrans: { [key in string]: string } = t('milvus');
+  const { t: commonTrans } = useTranslation();
+  const milvusTrans = commonTrans('milvus');
 
   useEffect(() => {
     if (defaultActive) {
@@ -176,12 +174,10 @@ const NavMenu: FC<NavMenuType> = props => {
               button
               key={v.label}
               title={v.label}
-              className={
-                clsx(classes.item, {
-                  [className]: className,
-                  [classes.active]: isActive,
-                })
-              }
+              className={clsx(classes.item, {
+                [className]: className,
+                [classes.active]: isActive,
+              })}
               onClick={() => {
                 setActive(v.label);
                 v.onClick && v.onClick();
@@ -192,13 +188,11 @@ const NavMenu: FC<NavMenuType> = props => {
               </ListItemIcon>
 
               <Fade in={expanded} timeout={timeout}>
-                <ListItemText className={classes.itemText}
-                  primary={v.label} />
+                <ListItemText className={classes.itemText} primary={v.label} />
               </Fade>
             </ListItem>
           );
-        })
-        }
+        })}
       </>
     );
   };
@@ -206,22 +200,21 @@ const NavMenu: FC<NavMenuType> = props => {
   const Logo = icons.milvus;
 
   return (
-    <List component="nav" className={
-      clsx(classes.root, {
+    <List
+      component="nav"
+      className={clsx(classes.root, {
         [classes.rootExpand]: expanded,
         [classes.rootCollapse]: !expanded,
-      })
-    }>
+      })}
+    >
       <div>
         <div className={classes.logoWrapper}>
           <Logo
             classes={{ root: classes.logo }}
-            className={
-              clsx({
-                [classes.logoExpand]: expanded,
-                [classes.logoCollapse]: !expanded,
-              })
-            }
+            className={clsx({
+              [classes.logoExpand]: expanded,
+              [classes.logoCollapse]: !expanded,
+            })}
           />
           <Fade in={expanded} timeout={timeout}>
             <Typography variant="h3" className="title">
@@ -230,17 +223,18 @@ const NavMenu: FC<NavMenuType> = props => {
           </Fade>
         </div>
         <Button
-          onClick={() => { setExpanded(!expanded) }}
-          className={
-            clsx(classes.actionIcon, {
-              [classes.expandIcon]: expanded,
-              [classes.collapseIcon]: !expanded,
-            })
-          }
+          onClick={() => {
+            setExpanded(!expanded);
+          }}
+          className={clsx(classes.actionIcon, {
+            [classes.expandIcon]: expanded,
+            [classes.collapseIcon]: !expanded,
+          })}
         >
           <ChevronRightIcon />
         </Button>
         <NestList data={data} />
+        <CommunityBtn />
       </div>
     </List>
   );

+ 2 - 2
client/src/components/status/Status.tsx

@@ -41,8 +41,8 @@ const useStyles = makeStyles((theme: Theme) =>
 
 const Status: FC<StatusType> = props => {
   const { status } = props;
-  const { t } = useTranslation();
-  const statusTrans: { [key in string]: string } = t('status');
+  const { t: commonTrans } = useTranslation();
+  const statusTrans = commonTrans('status');
   const { label, color } = useMemo(() => {
     switch (status) {
       case StatusEnum.unloaded:

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

@@ -39,9 +39,7 @@ export const AuthProvider = (props: { children: React.ReactNode }) => {
   }, [setAddress]);
 
   useEffect(() => {
-    document.title = address
-      ? `${address} - Milvus Insight`
-      : 'Milvus Insight';
+    document.title = address ? `${address} - Milvus Insight` : 'Milvus Insight';
   }, [address]);
 
   return (

+ 15 - 1
client/src/i18n/cn/common.ts

@@ -19,10 +19,24 @@ const commonTrans = {
   },
   copy: {
     copy: 'Copy',
-    copied: 'Copied',
+    copied: 'Copied!',
+    label: 'copy button',
   },
   param: 'Parameter',
   search: 'Search by name',
+  code: 'Code View',
+  view: 'View Code',
+  js: 'NODE.JS',
+  py: 'PYTHON',
+  community: {
+    hi: 'Hi, there!',
+    growing: 'Our growing community is here!',
+    question: 'Have question about Milvus?',
+    qr: 'Scan WeChat QR code to get access',
+    more: 'More Channels to Explore',
+    join: 'Join our growing social community today',
+    get: 'Get insight, tips and share ideas'
+  },
 };
 
 export default commonTrans;

+ 15 - 1
client/src/i18n/en/common.ts

@@ -19,10 +19,24 @@ const commonTrans = {
   },
   copy: {
     copy: 'Copy',
-    copied: 'Copied',
+    copied: 'Copied!',
+    label: 'copy button',
   },
   param: 'Parameter',
   search: 'Search by name',
+  code: 'Code View',
+  view: 'View Code',
+  js: 'NODE.JS',
+  py: 'PYTHON',
+  community: {
+    hi: 'Hi, there!',
+    growing: 'Our growing community is here!',
+    question: 'Have question about Milvus?',
+    qr: 'Scan WeChat QR code to get access',
+    more: 'More Channels to Explore',
+    join: 'Join our growing social community today',
+    get: 'Get insight, tips and share ideas'
+  },
 };
 
 export default commonTrans;

+ 6 - 1
client/src/index.css

@@ -10,6 +10,11 @@ body {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+  font-family: 'Source Code Pro', Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}
+
+code span {
+  font-family: 'Source Code Pro', Menlo, Monaco, Consolas, 'Courier New',
     monospace;
 }

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

@@ -55,7 +55,7 @@ const Connect = () => {
   const classes = useStyles();
   const { t: commonTrans } = useTranslation();
   const { t: warningTrans } = useTranslation('warning');
-  const milvusTrans: { [key in string]: string } = commonTrans('milvus');
+  const milvusTrans = commonTrans('milvus');
   const { t: btnTrans } = useTranslation('btn');
   const { t: successTrans } = useTranslation('success');
 

+ 5 - 1
client/src/pages/overview/collectionCard/CollectionCard.tsx

@@ -26,7 +26,7 @@ const useStyles = makeStyles((theme: Theme) => ({
 
     margin: theme.spacing(2, 0),
 
-    color: '#010e29',
+    color: theme.palette.milvusDark.main,
     fontSize: '20px',
     lineHeight: '24px',
     fontWeight: 'bold',
@@ -64,6 +64,10 @@ const useStyles = makeStyles((theme: Theme) => ({
   },
   btn: {
     marginRight: theme.spacing(1),
+    padding: theme.spacing(0.5, 1),
+
+    lineHeight: '20px',
+    fontSize: 14,
   },
 }));
 

+ 1 - 1
client/src/pages/overview/statisticsCard/StatisticsCard.tsx

@@ -17,7 +17,7 @@ const useStyles = makeStyles((theme: Theme) => ({
   label: {
     fontSize: '12px',
     lineHeight: '16px',
-    color: '#010e29',
+    color: theme.palette.milvusDark.main,
   },
   value: {
     fontSize: '24px',

+ 51 - 11
client/src/pages/schema/Create.tsx

@@ -1,6 +1,8 @@
 import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
+import { CodeLanguageEnum, CodeViewData } from '../../components/code/Types';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
+import CustomSwitch from '../../components/customSwitch/CustomSwitch';
 import {
   INDEX_CONFIG,
   INDEX_OPTIONS_MAP,
@@ -8,6 +10,8 @@ import {
   METRIC_TYPES_VALUES,
 } from '../../consts/Milvus';
 import { useFormValidation } from '../../hooks/Form';
+import { getCreateIndexJSCode } from '../../utils/code/Js';
+import { getCreateIndexPYCode } from '../../utils/code/Py';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { getEmbeddingType } from '../../utils/search';
 import { DataType } from '../collections/Types';
@@ -19,12 +23,17 @@ const CreateIndex = (props: {
   fieldType: DataType;
   handleCreate: (params: IndexExtraParam) => void;
   handleCancel: () => void;
+
+  // used for code mode
+  fieldName: string;
 }) => {
-  const { collectionName, fieldType, handleCreate, handleCancel } = props;
+  const { collectionName, fieldType, handleCreate, handleCancel, fieldName } =
+    props;
 
   const { t: indexTrans } = useTranslation('index');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: btnTrans } = useTranslation('btn');
+  const { t: commonTrans } = useTranslation();
 
   const defaultIndexType =
     fieldType === 'BinaryVector'
@@ -53,6 +62,9 @@ const CreateIndex = (props: {
     knng: '',
   });
 
+  // control whether show code mode
+  const [showCode, setShowCode] = useState<boolean>(false);
+
   const indexCreateParams = useMemo(() => {
     if (!INDEX_CONFIG[indexSetting.index_type]) {
       return [];
@@ -65,12 +77,21 @@ const CreateIndex = (props: {
     [indexSetting.index_type, fieldType]
   );
 
-  const indexParams = useMemo(() => {
+  const extraParams = useMemo(() => {
     const params: { [x: string]: string } = {};
     indexCreateParams.forEach(v => {
       params[v] = indexSetting[v];
     });
-    return params;
+
+    const { index_type, metric_type } = indexSetting;
+  
+    const extraParams: IndexExtraParam = {
+      index_type,
+      metric_type,
+      params: JSON.stringify(params),
+    };
+
+    return extraParams;
   }, [indexCreateParams, indexSetting]);
 
   const indexOptions = useMemo(() => {
@@ -87,6 +108,25 @@ const CreateIndex = (props: {
     return form;
   }, [indexSetting, indexCreateParams]);
 
+  /**
+   * create index code mode
+   */
+  const codeBlockData: CodeViewData[] = useMemo(
+    () => [
+      {
+        label: commonTrans('py'),
+        language: CodeLanguageEnum.python,
+        code: getCreateIndexPYCode({ collectionName, fieldName, extraParams }),
+      },
+      {
+        label: commonTrans('js'),
+        language: CodeLanguageEnum.javascript,
+        code: getCreateIndexJSCode({ collectionName, fieldName, extraParams }),
+      },
+    ],
+    [commonTrans, extraParams, collectionName, fieldName]
+  );
+
   const { validation, checkIsValid, disabled, setDisabled, resetValidation } =
     useFormValidation(checkedForm);
 
@@ -128,15 +168,12 @@ const CreateIndex = (props: {
   };
 
   const handleCreateIndex = () => {
-    const { index_type, metric_type } = indexSetting;
-
-    const params: IndexExtraParam = {
-      index_type,
-      metric_type,
-      params: JSON.stringify(indexParams),
-    };
+    handleCreate(extraParams);
+  };
 
-    handleCreate(params);
+  const handleShowCode = (event: React.ChangeEvent<{ checked: boolean }>) => {
+    const isChecked = event.target.checked;
+    setShowCode(isChecked);
   };
 
   return (
@@ -149,6 +186,9 @@ const CreateIndex = (props: {
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateIndex}
       confirmDisabled={disabled}
+      leftActions={<CustomSwitch onChange={handleShowCode} />}
+      showCode={showCode}
+      codeBlocksData={codeBlockData}
     >
       <CreateForm
         updateForm={updateStepTwoForm}

+ 23 - 4
client/src/pages/schema/IndexTypeElement.tsx

@@ -1,4 +1,11 @@
-import { FC, useCallback, useContext, useEffect, useState } from 'react';
+import {
+  FC,
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react';
 import Chip from '@material-ui/core/Chip';
 import { IndexHttp } from '../../http/Index';
 import { IndexState } from '../../types/Milvus';
@@ -91,6 +98,11 @@ const IndexTypeElement: FC<{
   const AddIcon = icons.add;
   const DeleteIcon = icons.delete;
 
+  const isIndexCreating: boolean = useMemo(
+    () => status === IndexState.InProgress || status === IndexState.Unissued,
+    [status]
+  );
+
   const fetchStatus = useCallback(async () => {
     // prevent delete index trigger fetching index status
     if (data._indexType !== '' && status !== IndexState.Delete) {
@@ -106,7 +118,7 @@ const IndexTypeElement: FC<{
     if (timer) {
       clearTimeout(timer);
     }
-    if (data._indexType !== '' && status === IndexState.InProgress) {
+    if (data._indexType !== '' && isIndexCreating) {
       timer = setTimeout(async () => {
         const res = await IndexHttp.getIndexBuildProgress(
           collectionName,
@@ -129,7 +141,7 @@ const IndexTypeElement: FC<{
         }
       }, 500);
     }
-  }, [collectionName, data._fieldName, status, data._indexType]);
+  }, [collectionName, data._fieldName, isIndexCreating, data._indexType]);
 
   useEffect(() => {
     /**
@@ -161,6 +173,7 @@ const IndexTypeElement: FC<{
         component: (
           <CreateIndex
             collectionName={collectionName}
+            fieldName={data._fieldName}
             fieldType={data._fieldType}
             handleCancel={handleCloseDialog}
             handleCreate={requestCreateIndex}
@@ -202,6 +215,7 @@ const IndexTypeElement: FC<{
   };
 
   const generateElement = () => {
+    // only vector type field is able to create index
     if (
       data._fieldType !== 'BinaryVector' &&
       data._fieldType !== 'FloatVector'
@@ -209,6 +223,7 @@ const IndexTypeElement: FC<{
       return <div className={classes.item}>--</div>;
     }
 
+    // _indexType example: FLAT
     switch (data._indexType) {
       case '': {
         return (
@@ -232,7 +247,11 @@ const IndexTypeElement: FC<{
         if (status === IndexState.Default || status === IndexState.Delete) {
           return <StatusIcon type={ChildrenStatusType.CREATING} />;
         }
-        return status === IndexState.InProgress ? (
+        /**
+         * if creating not finished, show progress bar
+         * if creating finished, show chip that contains index type
+         */
+        return isIndexCreating ? (
           <CustomLinearProgress
             value={createProgress}
             tooltip={indexTrans('creating')}

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

@@ -45,7 +45,7 @@ const useStyles = makeStyles((theme: Theme) => ({
       },
 
       '& .value': {
-        color: '#010e29',
+        color: theme.palette.milvusDark.main,
       },
     },
   },

+ 27 - 3
client/src/pages/seach/VectorSearch.tsx

@@ -94,11 +94,28 @@ const VectorSearch = () => {
     return nonVectorFields.map(f => f._fieldName);
   }, [selectedCollection, collections]);
 
+  const primaryKeyField = useMemo(() => {
+    const selectedCollectionInfo = collections.find(
+      c => c._name === selectedCollection
+    );
+    const fields = selectedCollectionInfo?._fields || [];
+    return fields.find(f => f._isPrimaryKey)?._fieldName;
+  }, [selectedCollection, collections]);
+
   const colDefinitions: ColDefinitionsType[] = useMemo(() => {
-    // filter id and score
+    /**
+     * id represents primary key, score represents distance
+     * since we transfer score to distance in the view, and original field which is primary key has already in the table
+     * we filter 'id' and 'score' to avoid redundant data
+     */
     return searchResult && searchResult.length > 0
       ? Object.keys(searchResult[0])
-          .filter(item => item !== 'id' && item !== 'score')
+          .filter(item => {
+            // if primary key field name is id, don't filter it
+            const invalidItems =
+              primaryKeyField === 'id' ? ['score'] : ['id', 'score'];
+            return !invalidItems.includes(item);
+          })
           .map(key => ({
             id: key,
             align: 'left',
@@ -106,7 +123,7 @@ const VectorSearch = () => {
             label: key,
           }))
       : [];
-  }, [searchResult]);
+  }, [searchResult, primaryKeyField]);
 
   const {
     metricType,
@@ -201,6 +218,12 @@ const VectorSearch = () => {
       // only vector type fields can be select
       const fieldOptions = getVectorFieldOptions(vectorFields, indexes);
       setFieldOptions(fieldOptions);
+      if (fieldOptions.length > 0) {
+        // set first option value as default field value
+        const [{ value: defaultFieldValue }] = fieldOptions;
+        setSelectedField(defaultFieldValue as string);
+      }
+
       // only non vector type fields can be advanced filter
       const filterFields = getNonVectorFieldsForFilter(nonVectorFields);
       setFilterFields(filterFields);
@@ -353,6 +376,7 @@ const VectorSearch = () => {
             value={selectedCollection}
             onChange={(e: { target: { value: unknown } }) => {
               const collection = e.target.value;
+
               setSelectedCollection(collection as string);
               // every time selected collection changed, reset field
               setSelectedField('');

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

@@ -61,3 +61,17 @@ fieldset {
 .dialog-content::first-letter {
   text-transform: uppercase;
 }
+
+/* change scrollbar style */
+::-webkit-scrollbar {
+  width: 8px;
+}
+
+::-webkit-scrollbar-track {
+  background-color: #f9f9f9;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 8px;
+  background-color: #eee;
+}

+ 5 - 0
client/src/styles/theme.ts

@@ -7,10 +7,12 @@ declare module '@material-ui/core/styles/createPalette' {
   interface Palette {
     milvusBlue: Palette['primary'];
     milvusGrey: Palette['primary'];
+    milvusDark: Palette['primary'];
   }
   interface PaletteOptions {
     milvusBlue: PaletteOptions['primary'];
     milvusGrey: PaletteOptions['primary'];
+    milvusDark: PaletteOptions['primary'];
   }
 }
 
@@ -55,6 +57,9 @@ const commonThemes = {
       dark: '#82838e',
       contrastText: '#f8f8fc',
     },
+    milvusDark: {
+      main: '#010e29',
+    },
   },
   breakpoints: {
     values: {

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

@@ -91,6 +91,20 @@ export const getKeyValuePairFromObj = (
   return pairs;
 };
 
+/**
+ * @param pairs e.g. [{key: 'key', value: 'value'}]
+ * @returns object, e.g. {key: value}
+ */
+export const getObjFromKeyValuePair = (
+  pairs: { key: string; value: any }[]
+): { [key in string]: any } => {
+  const obj = pairs.reduce((acc, cur) => {
+    acc[cur.key] = cur.value;
+    return acc;
+  }, {} as { [key in string]: any });
+  return obj;
+};
+
 export const getKeyValueListFromJsonString = (
   json: string
 ): { key: string; value: string }[] => {

+ 16 - 0
client/src/utils/code/Js.ts

@@ -0,0 +1,16 @@
+import { CreateIndexCodeParam } from './Types';
+
+export const getCreateIndexJSCode = (params: CreateIndexCodeParam) => {
+  const { collectionName, fieldName, extraParams } = params;
+
+  const jsCode = `import { MilvusClient } from '@zilliz/milvus2-sdk-node';
+const client = new MilvusClient(milvus_address);
+
+client.indexManager.createIndex({
+  collection_name: '${collectionName}',
+  field_name: '${fieldName}',
+  extra_params: ${JSON.stringify(extraParams, null, 2)},
+});`;
+
+  return jsCode;
+};

+ 25 - 0
client/src/utils/code/Py.ts

@@ -0,0 +1,25 @@
+import { parseValue } from '../Insert';
+import { CreateIndexCodeParam } from './Types';
+
+// use replacer to parse extra param from JSON to original object
+const replacer = (key: string, value: any) => {
+  if (typeof value === 'string') {
+    return parseValue(value);
+  }
+  return value;
+};
+
+export const getCreateIndexPYCode = (params: CreateIndexCodeParam) => {
+  const { collectionName, fieldName, extraParams } = params;
+  const index = {
+    ...extraParams,
+    params: parseValue(extraParams.params),
+  };
+  const pyCode = `from pymilvus_orm import Collection
+
+collection = Collection('${collectionName}')
+index = ${JSON.stringify(index, replacer, 4)}
+collection.create_index(${fieldName}, index)`;
+
+  return pyCode;
+};

+ 6 - 0
client/src/utils/code/Types.ts

@@ -0,0 +1,6 @@
+import { IndexExtraParam } from "../../pages/schema/Types";
+export interface CreateIndexCodeParam {
+  collectionName: string;
+  fieldName: string;
+  extraParams: IndexExtraParam
+}

+ 1 - 1
client/src/utils/search.ts

@@ -19,8 +19,8 @@ export const transferSearchResult = (
     .sort((a, b) => a.score - b.score)
     .map((r, index) => ({
       rank: index + 1,
-      distance: r.score,
       ...r,
+      distance: r.score,
     }));
 
   return resultView;

+ 157 - 0
client/yarn.lock

@@ -1869,6 +1869,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/hast@^2.0.0":
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.2.tgz#236201acca9e2695e42f713d7dd4f151dc2982e4"
+  integrity sha512-Op5W7jYgZI7AWKY5wQ0/QNMzQM7dGQPyW1rXKNiymVCy5iTfdPuGu4HhYNOM2sIv8gUfIuIdcYlXmAepwaowow==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/history@*":
   version "4.7.8"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
@@ -2001,6 +2008,13 @@
     "@types/history" "*"
     "@types/react" "*"
 
+"@types/react-syntax-highlighter@^13.5.2":
+  version "13.5.2"
+  resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-13.5.2.tgz#357cc03581dc434c57c3b31f70e0eecdbf7b3ab0"
+  integrity sha512-sRZoKZBGKaE7CzMvTTgz+0x/aVR58ZYUTfB7HN76vC+yQnvo1FWtzNARBt0fGqcLGEVakEzMu/CtPzssmanu8Q==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-test-renderer@>=16.9.0":
   version "17.0.1"
   resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b"
@@ -2074,6 +2088,11 @@
   dependencies:
     source-map "^0.6.1"
 
+"@types/unist@*":
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
+  integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
+
 "@types/webpack-sources@*":
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.1.0.tgz#8882b0bd62d1e0ce62f183d0d01b72e6e82e8c10"
@@ -3398,6 +3417,21 @@ char-regex@^1.0.2:
   resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
   integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
 
+character-entities-legacy@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
+  integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
+
+character-entities@^1.0.0:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
+  integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
+
+character-reference-invalid@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
+  integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
+
 check-types@^11.1.1:
   version "11.1.2"
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
@@ -3594,6 +3628,11 @@ combined-stream@^1.0.8:
   dependencies:
     delayed-stream "~1.0.0"
 
+comma-separated-tokens@^1.0.0:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
+  integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -5144,6 +5183,13 @@ fastq@^1.6.0:
   dependencies:
     reusify "^1.0.4"
 
+fault@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
+  integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
+  dependencies:
+    format "^0.2.0"
+
 faye-websocket@^0.11.3:
   version "0.11.4"
   resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
@@ -5316,6 +5362,11 @@ form-data@^3.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+format@^0.2.0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
+  integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
+
 forwarded@0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -5660,6 +5711,22 @@ hash.js@^1.0.0, hash.js@^1.0.3:
     inherits "^2.0.3"
     minimalistic-assert "^1.0.1"
 
+hast-util-parse-selector@^2.0.0:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
+  integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
+
+hastscript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
+  integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    comma-separated-tokens "^1.0.0"
+    hast-util-parse-selector "^2.0.0"
+    property-information "^5.0.0"
+    space-separated-tokens "^1.0.0"
+
 he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -5675,6 +5742,11 @@ highlight-words-core@^1.2.0:
   resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa"
   integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==
 
+highlight.js@^10.4.1, highlight.js@~10.7.0:
+  version "10.7.3"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
+  integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+
 history@^4.9.0:
   version "4.10.1"
   resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
@@ -6100,6 +6172,19 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
+is-alphabetical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
+  integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
+
+is-alphanumerical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
+  integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
+  dependencies:
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+
 is-arguments@^1.0.4:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
@@ -6198,6 +6283,11 @@ is-date-object@^1.0.1:
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5"
   integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==
 
+is-decimal@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
+  integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
+
 is-descriptor@^0.1.0:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
@@ -6272,6 +6362,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
   dependencies:
     is-extglob "^2.1.1"
 
+is-hexadecimal@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
+  integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
+
 is-in-browser@^1.0.2, is-in-browser@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
@@ -7339,6 +7434,14 @@ lower-case@^2.0.2:
   dependencies:
     tslib "^2.0.3"
 
+lowlight@^1.17.0:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888"
+  integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==
+  dependencies:
+    fault "^1.0.0"
+    highlight.js "~10.7.0"
+
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -8203,6 +8306,18 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5:
     pbkdf2 "^3.0.3"
     safe-buffer "^5.1.1"
 
+parse-entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
+  integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
+  dependencies:
+    character-entities "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    character-reference-invalid "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-hexadecimal "^1.0.0"
+
 parse-json@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -9121,6 +9236,11 @@ prepend-http@^1.0.0:
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
   integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
 
+prettier@2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
+  integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
+
 pretty-bytes@^5.3.0:
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -9144,6 +9264,11 @@ pretty-format@^26.0.0, pretty-format@^26.6.0, pretty-format@^26.6.2:
     ansi-styles "^4.0.0"
     react-is "^17.0.1"
 
+prismjs@^1.22.0, prismjs@~1.24.0:
+  version "1.24.1"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036"
+  integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==
+
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -9196,6 +9321,13 @@ prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
     object-assign "^4.1.1"
     react-is "^16.8.1"
 
+property-information@^5.0.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
+  integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==
+  dependencies:
+    xtend "^4.0.0"
+
 proxy-addr@~2.0.5:
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -9543,6 +9675,17 @@ react-scripts@4.0.3:
   optionalDependencies:
     fsevents "^2.1.3"
 
+react-syntax-highlighter@^15.4.4:
+  version "15.4.4"
+  resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.4.4.tgz#dc9043f19e7bd063ff3ea78986d22a6eaa943b2a"
+  integrity sha512-PsOFHNTzkb3OroXdoR897eKN5EZ6grht1iM+f1lJSq7/L0YVnkJaNVwC3wEUYPOAmeyl5xyer1DjL6MrumO6Zw==
+  dependencies:
+    "@babel/runtime" "^7.3.1"
+    highlight.js "^10.4.1"
+    lowlight "^1.17.0"
+    prismjs "^1.22.0"
+    refractor "^3.2.0"
+
 react-transition-group@^4.4.0:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
@@ -9650,6 +9793,15 @@ redent@^3.0.0:
     indent-string "^4.0.0"
     strip-indent "^3.0.0"
 
+refractor@^3.2.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.4.0.tgz#62bd274b06c942041f390c371b676eb67cb0a678"
+  integrity sha512-dBeD02lC5eytm9Gld2Mx0cMcnR+zhSnsTfPpWqFaMgUMJfC9A6bcN3Br/NaXrnBJcuxnLFR90k1jrkaSyV8umg==
+  dependencies:
+    hastscript "^6.0.0"
+    parse-entities "^2.0.0"
+    prismjs "~1.24.0"
+
 regenerate-unicode-properties@^8.2.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@@ -10422,6 +10574,11 @@ sourcemap-codec@^1.4.4:
   resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
   integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
 
+space-separated-tokens@^1.0.0:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
+  integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
+
 spdx-correct@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"