Переглянути джерело

Merge pull request #2 from sutcalag/mergify/shanghaikid/config-update

Mergify/shanghaikid/config update
zhuanghong.chen 3 роки тому
батько
коміт
7f1da5cc6b
52 змінених файлів з 820 додано та 343 видалено
  1. 7 0
      .mergify.yml
  2. 3 1
      README.md
  3. 1 1
      client/.prettierrc
  4. 6 2
      client/package.json
  5. 4 0
      client/public/index.html
  6. 0 38
      client/src/components/__test__/copy/Copy.spec.tsx
  7. 10 11
      client/src/components/advancedSearch/CopyButton.tsx
  8. 1 0
      client/src/components/advancedSearch/Types.ts
  9. 1 1
      client/src/components/advancedSearch/index.tsx
  10. 63 0
      client/src/components/code/CodeBlock.tsx
  11. 113 0
      client/src/components/code/CodeView.tsx
  12. 20 0
      client/src/components/code/Types.ts
  13. 0 55
      client/src/components/copy/Copy.tsx
  14. 27 7
      client/src/components/customButton/CustomIconButton.tsx
  15. 9 6
      client/src/components/customDialog/CustomDialog.tsx
  16. 1 4
      client/src/components/customDialog/CustomDialogTitle.tsx
  17. 1 0
      client/src/components/customDialog/DeleteDialogTemplate.tsx
  18. 78 28
      client/src/components/customDialog/DialogTemplate.tsx
  19. 4 0
      client/src/components/customDialog/Types.ts
  20. 1 1
      client/src/components/customProgress/CustomLinearProgress.tsx
  21. 33 0
      client/src/components/customSwitch/CustomSwitch.tsx
  22. 3 0
      client/src/components/customSwitch/Types.ts
  23. 4 2
      client/src/components/customTabList/CustomTabList.tsx
  24. 1 0
      client/src/components/customTabList/Types.ts
  25. 5 2
      client/src/components/grid/Grid.tsx
  26. 13 2
      client/src/components/grid/Table.tsx
  27. 4 2
      client/src/components/grid/TablePaginationActions.tsx
  28. 2 1
      client/src/components/grid/ToolBar.tsx
  29. 2 2
      client/src/components/insert/Import.tsx
  30. 0 105
      client/src/components/layout/GlobalToolbar.tsx
  31. 2 2
      client/src/components/layout/Header.tsx
  32. 24 30
      client/src/components/menu/NavMenu.tsx
  33. 2 2
      client/src/components/status/Status.tsx
  34. 1 3
      client/src/context/Auth.tsx
  35. 6 1
      client/src/i18n/cn/common.ts
  36. 6 1
      client/src/i18n/en/common.ts
  37. 6 1
      client/src/index.css
  38. 1 1
      client/src/pages/connect/Connect.tsx
  39. 5 1
      client/src/pages/overview/collectionCard/CollectionCard.tsx
  40. 1 1
      client/src/pages/overview/statisticsCard/StatisticsCard.tsx
  41. 60 20
      client/src/pages/schema/Create.tsx
  42. 23 4
      client/src/pages/schema/IndexTypeElement.tsx
  43. 1 1
      client/src/pages/schema/Schema.tsx
  44. 21 3
      client/src/pages/seach/VectorSearch.tsx
  45. 14 0
      client/src/styles/common.css
  46. 5 0
      client/src/styles/theme.ts
  47. 14 0
      client/src/utils/Format.ts
  48. 16 0
      client/src/utils/code/Js.ts
  49. 27 0
      client/src/utils/code/Py.ts
  50. 10 0
      client/src/utils/code/Types.ts
  51. 1 1
      client/src/utils/search.ts
  52. 157 0
      client/yarn.lock

+ 7 - 0
.mergify.yml

@@ -0,0 +1,7 @@
+pull_request_rules:
+  - name: Automatic merge on approval
+    conditions:
+      - "#approved-reviews-by>=2"
+    actions:
+      merge:
+        method: merge

+ 3 - 1
README.md

@@ -1,4 +1,6 @@
 # Milvus insight
 # 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.
 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 |
 | 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                 |
 | 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
 #### Try the dev build
 
 

+ 1 - 1
.prettierrc → client/.prettierrc

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

+ 6 - 2
client/package.json

@@ -19,6 +19,7 @@
     "@types/react-dom": "^17.0.0",
     "@types/react-dom": "^17.0.0",
     "@types/react-highlight-words": "^0.16.2",
     "@types/react-highlight-words": "^0.16.2",
     "@types/react-router-dom": "^5.1.7",
     "@types/react-router-dom": "^5.1.7",
+    "@types/react-syntax-highlighter": "^13.5.2",
     "axios": "^0.21.1",
     "axios": "^0.21.1",
     "dayjs": "^1.10.5",
     "dayjs": "^1.10.5",
     "i18next": "^20.3.1",
     "i18next": "^20.3.1",
@@ -30,6 +31,7 @@
     "react-i18next": "^11.10.0",
     "react-i18next": "^11.10.0",
     "react-router-dom": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "react-scripts": "4.0.3",
     "react-scripts": "4.0.3",
+    "react-syntax-highlighter": "^15.4.4",
     "socket.io-client": "^4.1.3",
     "socket.io-client": "^4.1.3",
     "typescript": "^4.1.2",
     "typescript": "^4.1.2",
     "web-vitals": "^1.0.1"
     "web-vitals": "^1.0.1"
@@ -44,7 +46,8 @@
     "test:watch": "react-app-rewired test --watch",
     "test:watch": "react-app-rewired test --watch",
     "test:cov": "react-app-rewired test --watchAll=false --coverage",
     "test:cov": "react-app-rewired test --watchAll=false --coverage",
     "test:report": "react-app-rewired test --watchAll=false --coverage --coverageReporters='text-summary'",
     "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": {
   "eslintConfig": {
     "extends": [
     "extends": [
@@ -65,6 +68,7 @@
     ]
     ]
   },
   },
   "devDependencies": {
   "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"
       href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap"
       rel="stylesheet"
       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>
     <script src="%PUBLIC_URL%/env-config.js"></script>
 
 
     <!--
     <!--

+ 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 { CopyButtonProps } from './Types';
 import icons from '../icons/Icons';
 import icons from '../icons/Icons';
 import CustomIconButton from '../customButton/CustomIconButton';
 import CustomIconButton from '../customButton/CustomIconButton';
+import { useTranslation } from 'react-i18next';
 
 
 const CopyIcon = icons.copyExpression;
 const CopyIcon = icons.copyExpression;
 
 
 const CopyButton: FC<CopyButtonProps> = props => {
 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 classes = useStyles();
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
   const [tooltipTitle, setTooltipTitle] = useState('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);
     navigator.clipboard.writeText(v);
     setTimeout(() => {
     setTimeout(() => {
-      setTooltipTitle('Copy');
+      setTooltipTitle(copyTrans.copy);
     }, 1000);
     }, 1000);
   };
   };
 
 
@@ -30,7 +29,7 @@ const CopyButton: FC<CopyButtonProps> = props => {
       tooltip={tooltipTitle}
       tooltip={tooltipTitle}
       aria-label={label}
       aria-label={label}
       className={`${classes.button} ${className}`}
       className={`${classes.button} ${className}`}
-      onClick={() => handleClick(value || '')}
+      onClick={event => handleClick(event, value || '')}
       {...others}
       {...others}
     >
     >
       {icon || <CopyIcon style={{ color: 'transparent' }} />}
       {icon || <CopyIcon style={{ color: 'transparent' }} />}

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

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

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

@@ -1,3 +1,3 @@
 import Filter from './Filter';
 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 CustomIconButton = (props: IconButtonProps & { tooltip?: string }) => {
-  const { tooltip, ...otherProps } = props;
+  const { tooltip, className, ...otherProps } = props;
+  const classes = getStyles();
 
 
   return (
   return (
-    <>
+    <div className={`${classes.wrapper} ${className}`}>
       {tooltip ? (
       {tooltip ? (
-        <Tooltip title={tooltip}>
+        <Tooltip title={tooltip} arrow>
           <span>
           <span>
-            <IconButton {...otherProps}>{props.children}</IconButton>
+            <IconButton classes={{ root: classes.iconBtn }} {...otherProps}>
+              {props.children}
+            </IconButton>
           </span>
           </span>
         </Tooltip>
         </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) =>
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
   createStyles({
     paper: {
     paper: {
-      minWidth: '480px',
-      borderRadius: '8px',
+      minWidth: 480,
+      borderRadius: 8,
       padding: 0,
       padding: 0,
+
+      backgroundColor: 'transparent',
     },
     },
     noticePaper: {
     noticePaper: {
-      maxWidth: '480px',
+      backgroundColor: '#fff',
+      maxWidth: 480,
     },
     },
     paperSm: {
     paperSm: {
       maxWidth: '80%',
       maxWidth: '80%',
@@ -31,12 +34,12 @@ const useStyles = makeStyles((theme: Theme) =>
     },
     },
     title: {
     title: {
       '& p': {
       '& p': {
-        fontWeight: '500',
+        fontWeight: 500,
         overflow: 'hidden',
         overflow: 'hidden',
         textOverflow: 'ellipsis',
         textOverflow: 'ellipsis',
         whiteSpace: 'nowrap',
         whiteSpace: 'nowrap',
-        maxWidth: '300px',
-        fontSize: '20px',
+        maxWidth: 300,
+        fontSize: 20,
       },
       },
     },
     },
     padding: {
     padding: {

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

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

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

@@ -16,6 +16,7 @@ import { rootContext } from '../../context/Root';
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   root: {
   root: {
     maxWidth: '480px',
     maxWidth: '480px',
+    backgroundColor: '#fff',
   },
   },
   mb: {
   mb: {
     marginBottom: theme.spacing(2.5),
     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 { useTranslation } from 'react-i18next';
 import {
 import {
   DialogContent,
   DialogContent,
@@ -9,8 +9,29 @@ import {
 import { DialogContainerProps } from './Types';
 import { DialogContainerProps } from './Types';
 import CustomDialogTitle from './CustomDialogTitle';
 import CustomDialogTitle from './CustomDialogTitle';
 import CustomButton from '../customButton/CustomButton';
 import CustomButton from '../customButton/CustomButton';
+import CodeView from '../code/CodeView';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 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: {
   actions: {
     paddingTop: theme.spacing(2),
     paddingTop: theme.spacing(2),
     justifyContent: 'space-between',
     justifyContent: 'space-between',
@@ -30,41 +51,70 @@ const DialogTemplate: FC<DialogContainerProps> = ({
   showCancel = true,
   showCancel = true,
   showCloseIcon = true,
   showCloseIcon = true,
   leftActions,
   leftActions,
+  // needed for code mode
+  showCode = false,
+  codeBlocksData = [],
 }) => {
 }) => {
   const { t } = useTranslation('btn');
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
   const cancel = cancelLabel || t('cancel');
   const confirm = confirmLabel || t('confirm');
   const confirm = confirmLabel || t('confirm');
-  const classes = useStyles();
+  const classes = useStyles({ showCode });
   const onCancel = handleCancel || handleClose;
   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 (
   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>
-            )}
-            <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 { ReactElement } from 'react';
 import { DialogType } from '../../context/Types';
 import { DialogType } from '../../context/Types';
+import { CodeViewData } from '../code/Types';
 export type CustomDialogType = DialogType & {
 export type CustomDialogType = DialogType & {
   onClose: () => void;
   onClose: () => void;
   containerClass?: string;
   containerClass?: string;
@@ -32,4 +33,7 @@ export type DialogContainerProps = {
   showActions?: boolean;
   showActions?: boolean;
   showCancel?: boolean;
   showCancel?: boolean;
   leftActions?: ReactElement;
   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: {
   percent: {
     minWidth: '35px',
     minWidth: '35px',
     marginLeft: theme.spacing(1),
     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 CustomTabList: FC<ITabListProps> = props => {
-  const { tabs, activeIndex = 0, handleTabChange } = props;
+  const { tabs, activeIndex = 0, handleTabChange, wrapperClass = '' } = props;
   const classes = useStyles();
   const classes = useStyles();
   const [value, setValue] = useState<number>(activeIndex);
   const [value, setValue] = useState<number>(activeIndex);
 
 
@@ -67,10 +67,12 @@ const CustomTabList: FC<ITabListProps> = props => {
     <>
     <>
       <Tabs
       <Tabs
         classes={{
         classes={{
-          root: classes.wrapper,
+          root: `${classes.wrapper} ${wrapperClass}`,
           indicator: classes.tab,
           indicator: classes.tab,
           flexContainer: classes.tabContainer,
           flexContainer: classes.tabContainer,
         }}
         }}
+        // if not provide this property, Material will add single span element by default
+        TabIndicatorProps={{ children: <div className="tab-indicator" /> }}
         value={value}
         value={value}
         onChange={handleChange}
         onChange={handleChange}
         aria-label="tabs"
         aria-label="tabs"

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

@@ -9,6 +9,7 @@ export interface ITabListProps {
   tabs: ITab[];
   tabs: ITab[];
   activeIndex?: number;
   activeIndex?: number;
   handleTabChange?: (index: number) => void;
   handleTabChange?: (index: number) => void;
+  wrapperClass?: string;
 }
 }
 
 
 export interface ITabPanel {
 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 MilvusGrid: FC<MilvusGridType> = props => {
   const classes = userStyle();
   const classes = userStyle();
-  const { t } = useTranslation();
-  const gridTrans = t('grid') as any;
+
+  // i18n
+  const { t: commonTrans } = useTranslation();
+  const gridTrans = commonTrans('grid');
+
   const {
   const {
     rowCount = 10,
     rowCount = 10,
     rowsPerPage = 5,
     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 EnhancedTableHead from './TableHead';
 import EditableTableHead from './TableEditableHead';
 import EditableTableHead from './TableEditableHead';
 import { stableSort, getComparator } from './Utils';
 import { stableSort, getComparator } from './Utils';
-import Copy from '../../components/copy/Copy';
 import ActionBar from './ActionBar';
 import ActionBar from './ActionBar';
 import LoadingTable from './LoadingTable';
 import LoadingTable from './LoadingTable';
+import CopyButton from '../advancedSearch/CopyButton';
+import { useTranslation } from 'react-i18next';
 
 
 const useStyles = makeStyles(theme => ({
 const useStyles = makeStyles(theme => ({
   root: {
   root: {
@@ -99,6 +100,9 @@ const useStyles = makeStyles(theme => ({
     letterSpacing: '0.5px',
     letterSpacing: '0.5px',
     color: 'rgba(0, 0, 0, 0.6)',
     color: 'rgba(0, 0, 0, 0.6)',
   },
   },
+  copyBtn: {
+    marginLeft: theme.spacing(0.5),
+  },
 }));
 }));
 
 
 const EnhancedTable: FC<TableType> = props => {
 const EnhancedTable: FC<TableType> = props => {
@@ -134,6 +138,9 @@ const EnhancedTable: FC<TableType> = props => {
 
 
   const containerRef = useRef(null);
   const containerRef = useRef(null);
 
 
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
+
   const handleRequestSort = (event: any, property: string) => {
   const handleRequestSort = (event: any, property: string) => {
     const isAsc = orderBy === property && order === 'asc';
     const isAsc = orderBy === property && order === 'asc';
     setOrder(isAsc ? 'desc' : 'asc');
     setOrder(isAsc ? 'desc' : 'asc');
@@ -313,7 +320,11 @@ const EnhancedTable: FC<TableType> = props => {
                               )}
                               )}
 
 
                               {needCopy && row[colDef.id] && (
                               {needCopy && row[colDef.id] && (
-                                <Copy data={row[colDef.id]} />
+                                <CopyButton
+                                  label={copyTrans.label}
+                                  value={row[colDef.id]}
+                                  className={classes.copyBtn}
+                                />
                               )}
                               )}
                             </TableCell>
                             </TableCell>
                           );
                           );

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

@@ -40,8 +40,10 @@ const useStyles = makeStyles((theme: Theme) =>
 const TablePaginationActions = (props: TablePaginationActionsProps) => {
 const TablePaginationActions = (props: TablePaginationActionsProps) => {
   const classes = useStyles();
   const classes = useStyles();
   const { count, page, rowsPerPage, onChangePage } = props;
   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 = (
   const handleBackButtonClick = (
     event: React.MouseEvent<HTMLButtonElement>
     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 Icon = c.icon ? Icons[c.icon!]() : '';
             const disabled = c.disabled ? c.disabled(selected) : false;
             const disabled = c.disabled ? c.disabled(selected) : false;
             // when disabled "disabledTooltip" will replace "tooltip"
             // 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 isIcon = c.type === 'iconBtn';
 
 
             const btn = (
             const btn = (

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

@@ -24,7 +24,7 @@ const getStyles = makeStyles((theme: Theme) => ({
       '& .selectLabel': {
       '& .selectLabel': {
         fontSize: '14px',
         fontSize: '14px',
         lineHeight: '20px',
         lineHeight: '20px',
-        color: '#010e29',
+        color: theme.palette.milvusDark.main,
       },
       },
 
 
       '& .divider': {
       '& .divider': {
@@ -187,7 +187,7 @@ const InsertImport: FC<InsertImportProps> = ({
           {insertTrans('noteTitle')}
           {insertTrans('noteTitle')}
         </Typography>
         </Typography>
         <ul className="noteList">
         <ul className="noteList">
-          {insertTrans('notes', { returnObjects: true }).map(note => (
+          {insertTrans('notes').map(note => (
             <li key={note} className="text noteItem">
             <li key={note} className="text noteItem">
               <Typography>{note}</Typography>
               <Typography>{note}</Typography>
             </li>
             </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 { navInfo } = useContext(navContext);
   const { address, setAddress } = useContext(authContext);
   const { address, setAddress } = useContext(authContext);
   const history = useHistory();
   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 BackIcon = icons.back;
   const LogoutIcon = icons.logout;
   const LogoutIcon = icons.logout;
 
 

+ 24 - 30
client/src/components/menu/NavMenu.tsx

@@ -138,7 +138,6 @@ const useStyles = makeStyles((theme: Theme) =>
     collapseIcon: {
     collapseIcon: {
       left: '73px',
       left: '73px',
     },
     },
-
   })
   })
 );
 );
 
 
@@ -148,8 +147,8 @@ const NavMenu: FC<NavMenuType> = props => {
   const [expanded, setExpanded] = useState<boolean>(false);
   const [expanded, setExpanded] = useState<boolean>(false);
   const [active, setActive] = useState<string>(defaultActive);
   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(() => {
   useEffect(() => {
     if (defaultActive) {
     if (defaultActive) {
@@ -175,12 +174,10 @@ const NavMenu: FC<NavMenuType> = props => {
               button
               button
               key={v.label}
               key={v.label}
               title={v.label}
               title={v.label}
-              className={
-                clsx(classes.item, {
-                  [className]: className,
-                  [classes.active]: isActive,
-                })
-              }
+              className={clsx(classes.item, {
+                [className]: className,
+                [classes.active]: isActive,
+              })}
               onClick={() => {
               onClick={() => {
                 setActive(v.label);
                 setActive(v.label);
                 v.onClick && v.onClick();
                 v.onClick && v.onClick();
@@ -191,13 +188,11 @@ const NavMenu: FC<NavMenuType> = props => {
               </ListItemIcon>
               </ListItemIcon>
 
 
               <Fade in={expanded} timeout={timeout}>
               <Fade in={expanded} timeout={timeout}>
-                <ListItemText className={classes.itemText}
-                  primary={v.label} />
+                <ListItemText className={classes.itemText} primary={v.label} />
               </Fade>
               </Fade>
             </ListItem>
             </ListItem>
           );
           );
-        })
-        }
+        })}
       </>
       </>
     );
     );
   };
   };
@@ -205,22 +200,21 @@ const NavMenu: FC<NavMenuType> = props => {
   const Logo = icons.milvus;
   const Logo = icons.milvus;
 
 
   return (
   return (
-    <List component="nav" className={
-      clsx(classes.root, {
+    <List
+      component="nav"
+      className={clsx(classes.root, {
         [classes.rootExpand]: expanded,
         [classes.rootExpand]: expanded,
         [classes.rootCollapse]: !expanded,
         [classes.rootCollapse]: !expanded,
-      })
-    }>
+      })}
+    >
       <div>
       <div>
         <div className={classes.logoWrapper}>
         <div className={classes.logoWrapper}>
           <Logo
           <Logo
             classes={{ root: classes.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}>
           <Fade in={expanded} timeout={timeout}>
             <Typography variant="h3" className="title">
             <Typography variant="h3" className="title">
@@ -229,13 +223,13 @@ const NavMenu: FC<NavMenuType> = props => {
           </Fade>
           </Fade>
         </div>
         </div>
         <Button
         <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 />
           <ChevronRightIcon />
         </Button>
         </Button>

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

@@ -41,8 +41,8 @@ const useStyles = makeStyles((theme: Theme) =>
 
 
 const Status: FC<StatusType> = props => {
 const Status: FC<StatusType> = props => {
   const { status } = 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(() => {
   const { label, color } = useMemo(() => {
     switch (status) {
     switch (status) {
       case StatusEnum.unloaded:
       case StatusEnum.unloaded:

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

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

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

@@ -19,10 +19,15 @@ const commonTrans = {
   },
   },
   copy: {
   copy: {
     copy: 'Copy',
     copy: 'Copy',
-    copied: 'Copied',
+    copied: 'Copied!',
+    label: 'copy button',
   },
   },
   param: 'Parameter',
   param: 'Parameter',
   search: 'Search by name',
   search: 'Search by name',
+  code: 'Code View',
+  view: 'View Code',
+  js: 'NODE.JS',
+  py: 'PYTHON',
   community: {
   community: {
     hi: 'Hi, there!',
     hi: 'Hi, there!',
     growing: 'Our growing community is here!',
     growing: 'Our growing community is here!',

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

@@ -19,10 +19,15 @@ const commonTrans = {
   },
   },
   copy: {
   copy: {
     copy: 'Copy',
     copy: 'Copy',
-    copied: 'Copied',
+    copied: 'Copied!',
+    label: 'copy button',
   },
   },
   param: 'Parameter',
   param: 'Parameter',
   search: 'Search by name',
   search: 'Search by name',
+  code: 'Code View',
+  view: 'View Code',
+  js: 'NODE.JS',
+  py: 'PYTHON',
   community: {
   community: {
     hi: 'Hi, there!',
     hi: 'Hi, there!',
     growing: 'Our growing community is here!',
     growing: 'Our growing community is here!',

+ 6 - 1
client/src/index.css

@@ -10,6 +10,11 @@ body {
 }
 }
 
 
 code {
 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;
     monospace;
 }
 }

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

@@ -55,7 +55,7 @@ const Connect = () => {
   const classes = useStyles();
   const classes = useStyles();
   const { t: commonTrans } = useTranslation();
   const { t: commonTrans } = useTranslation();
   const { t: warningTrans } = useTranslation('warning');
   const { t: warningTrans } = useTranslation('warning');
-  const milvusTrans: { [key in string]: string } = commonTrans('milvus');
+  const milvusTrans = commonTrans('milvus');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: successTrans } = useTranslation('success');
   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),
     margin: theme.spacing(2, 0),
 
 
-    color: '#010e29',
+    color: theme.palette.milvusDark.main,
     fontSize: '20px',
     fontSize: '20px',
     lineHeight: '24px',
     lineHeight: '24px',
     fontWeight: 'bold',
     fontWeight: 'bold',
@@ -64,6 +64,10 @@ const useStyles = makeStyles((theme: Theme) => ({
   },
   },
   btn: {
   btn: {
     marginRight: theme.spacing(1),
     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: {
   label: {
     fontSize: '12px',
     fontSize: '12px',
     lineHeight: '16px',
     lineHeight: '16px',
-    color: '#010e29',
+    color: theme.palette.milvusDark.main,
   },
   },
   value: {
   value: {
     fontSize: '24px',
     fontSize: '24px',

+ 60 - 20
client/src/pages/schema/Create.tsx

@@ -1,6 +1,8 @@
 import { useEffect, useMemo, useState } from 'react';
 import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import { CodeLanguageEnum, CodeViewData } from '../../components/code/Types';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
+import CustomSwitch from '../../components/customSwitch/CustomSwitch';
 import {
 import {
   INDEX_CONFIG,
   INDEX_CONFIG,
   INDEX_OPTIONS_MAP,
   INDEX_OPTIONS_MAP,
@@ -8,6 +10,8 @@ import {
   METRIC_TYPES_VALUES,
   METRIC_TYPES_VALUES,
 } from '../../consts/Milvus';
 } from '../../consts/Milvus';
 import { useFormValidation } from '../../hooks/Form';
 import { useFormValidation } from '../../hooks/Form';
+import { getCreateIndexJSCode } from '../../utils/code/Js';
+import { getCreateIndexPYCode } from '../../utils/code/Py';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { getEmbeddingType } from '../../utils/search';
 import { getEmbeddingType } from '../../utils/search';
 import { DataType } from '../collections/Types';
 import { DataType } from '../collections/Types';
@@ -19,12 +23,17 @@ const CreateIndex = (props: {
   fieldType: DataType;
   fieldType: DataType;
   handleCreate: (params: ParamPair[]) => void;
   handleCreate: (params: ParamPair[]) => void;
   handleCancel: () => 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: indexTrans } = useTranslation('index');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
+  const { t: commonTrans } = useTranslation();
 
 
   const defaultIndexType =
   const defaultIndexType =
     fieldType === 'BinaryVector'
     fieldType === 'BinaryVector'
@@ -53,6 +62,9 @@ const CreateIndex = (props: {
     knng: '',
     knng: '',
   });
   });
 
 
+  // control whether show code mode
+  const [showCode, setShowCode] = useState<boolean>(false);
+
   const indexCreateParams = useMemo(() => {
   const indexCreateParams = useMemo(() => {
     if (!INDEX_CONFIG[indexSetting.index_type]) {
     if (!INDEX_CONFIG[indexSetting.index_type]) {
       return [];
       return [];
@@ -65,12 +77,30 @@ const CreateIndex = (props: {
     [indexSetting.index_type, fieldType]
     [indexSetting.index_type, fieldType]
   );
   );
 
 
-  const indexParams = useMemo(() => {
+  const extraParams = useMemo(() => {
     const params: { [x: string]: string } = {};
     const params: { [x: string]: string } = {};
     indexCreateParams.forEach(v => {
     indexCreateParams.forEach(v => {
       params[v] = indexSetting[v];
       params[v] = indexSetting[v];
     });
     });
-    return params;
+
+    const { index_type, metric_type } = indexSetting;
+
+    const extraParams: ParamPair[] = [
+      {
+        key: 'index_type',
+        value: index_type,
+      },
+      {
+        key: 'metric_type',
+        value: metric_type,
+      },
+      {
+        key: 'params',
+        value: JSON.stringify(params),
+      },
+    ];
+
+    return extraParams;
   }, [indexCreateParams, indexSetting]);
   }, [indexCreateParams, indexSetting]);
 
 
   const indexOptions = useMemo(() => {
   const indexOptions = useMemo(() => {
@@ -87,6 +117,25 @@ const CreateIndex = (props: {
     return form;
     return form;
   }, [indexSetting, indexCreateParams]);
   }, [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 } =
   const { validation, checkIsValid, disabled, setDisabled, resetValidation } =
     useFormValidation(checkedForm);
     useFormValidation(checkedForm);
 
 
@@ -128,24 +177,12 @@ const CreateIndex = (props: {
   };
   };
 
 
   const handleCreateIndex = () => {
   const handleCreateIndex = () => {
-    const { index_type, metric_type } = indexSetting;
-
-    const params: ParamPair[] = [
-      {
-        key: 'index_type',
-        value: index_type,
-      },
-      {
-        key: 'metric_type',
-        value: metric_type,
-      },
-      {
-        key: 'params',
-        value: JSON.stringify(indexParams),
-      },
-    ];
+    handleCreate(extraParams);
+  };
 
 
-    handleCreate(params);
+  const handleShowCode = (event: React.ChangeEvent<{ checked: boolean }>) => {
+    const isChecked = event.target.checked;
+    setShowCode(isChecked);
   };
   };
 
 
   return (
   return (
@@ -158,6 +195,9 @@ const CreateIndex = (props: {
       confirmLabel={btnTrans('create')}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateIndex}
       handleConfirm={handleCreateIndex}
       confirmDisabled={disabled}
       confirmDisabled={disabled}
+      leftActions={<CustomSwitch onChange={handleShowCode} />}
+      showCode={showCode}
+      codeBlocksData={codeBlockData}
     >
     >
       <CreateForm
       <CreateForm
         updateForm={updateStepTwoForm}
         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 Chip from '@material-ui/core/Chip';
 import { IndexHttp } from '../../http/Index';
 import { IndexHttp } from '../../http/Index';
 import { IndexState } from '../../types/Milvus';
 import { IndexState } from '../../types/Milvus';
@@ -91,6 +98,11 @@ const IndexTypeElement: FC<{
   const AddIcon = icons.add;
   const AddIcon = icons.add;
   const DeleteIcon = icons.delete;
   const DeleteIcon = icons.delete;
 
 
+  const isIndexCreating: boolean = useMemo(
+    () => status === IndexState.InProgress || status === IndexState.Unissued,
+    [status]
+  );
+
   const fetchStatus = useCallback(async () => {
   const fetchStatus = useCallback(async () => {
     // prevent delete index trigger fetching index status
     // prevent delete index trigger fetching index status
     if (data._indexType !== '' && status !== IndexState.Delete) {
     if (data._indexType !== '' && status !== IndexState.Delete) {
@@ -106,7 +118,7 @@ const IndexTypeElement: FC<{
     if (timer) {
     if (timer) {
       clearTimeout(timer);
       clearTimeout(timer);
     }
     }
-    if (data._indexType !== '' && status === IndexState.InProgress) {
+    if (data._indexType !== '' && isIndexCreating) {
       timer = setTimeout(async () => {
       timer = setTimeout(async () => {
         const res = await IndexHttp.getIndexBuildProgress(
         const res = await IndexHttp.getIndexBuildProgress(
           collectionName,
           collectionName,
@@ -129,7 +141,7 @@ const IndexTypeElement: FC<{
         }
         }
       }, 500);
       }, 500);
     }
     }
-  }, [collectionName, data._fieldName, status, data._indexType]);
+  }, [collectionName, data._fieldName, isIndexCreating, data._indexType]);
 
 
   useEffect(() => {
   useEffect(() => {
     /**
     /**
@@ -161,6 +173,7 @@ const IndexTypeElement: FC<{
         component: (
         component: (
           <CreateIndex
           <CreateIndex
             collectionName={collectionName}
             collectionName={collectionName}
+            fieldName={data._fieldName}
             fieldType={data._fieldType}
             fieldType={data._fieldType}
             handleCancel={handleCloseDialog}
             handleCancel={handleCloseDialog}
             handleCreate={requestCreateIndex}
             handleCreate={requestCreateIndex}
@@ -202,6 +215,7 @@ const IndexTypeElement: FC<{
   };
   };
 
 
   const generateElement = () => {
   const generateElement = () => {
+    // only vector type field is able to create index
     if (
     if (
       data._fieldType !== 'BinaryVector' &&
       data._fieldType !== 'BinaryVector' &&
       data._fieldType !== 'FloatVector'
       data._fieldType !== 'FloatVector'
@@ -209,6 +223,7 @@ const IndexTypeElement: FC<{
       return <div className={classes.item}>--</div>;
       return <div className={classes.item}>--</div>;
     }
     }
 
 
+    // _indexType example: FLAT
     switch (data._indexType) {
     switch (data._indexType) {
       case '': {
       case '': {
         return (
         return (
@@ -232,7 +247,11 @@ const IndexTypeElement: FC<{
         if (status === IndexState.Default || status === IndexState.Delete) {
         if (status === IndexState.Default || status === IndexState.Delete) {
           return <StatusIcon type={ChildrenStatusType.CREATING} />;
           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
           <CustomLinearProgress
             value={createProgress}
             value={createProgress}
             tooltip={indexTrans('creating')}
             tooltip={indexTrans('creating')}

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

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

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

@@ -95,11 +95,28 @@ const VectorSearch = () => {
     return nonVectorFields.map(f => f._fieldName);
     return nonVectorFields.map(f => f._fieldName);
   }, [selectedCollection, collections]);
   }, [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(() => {
   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
     return searchResult && searchResult.length > 0
       ? Object.keys(searchResult[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 => ({
           .map(key => ({
             id: key,
             id: key,
             align: 'left',
             align: 'left',
@@ -107,7 +124,7 @@ const VectorSearch = () => {
             label: key,
             label: key,
           }))
           }))
       : [];
       : [];
-  }, [searchResult]);
+  }, [searchResult, primaryKeyField]);
 
 
   const {
   const {
     metricType,
     metricType,
@@ -367,6 +384,7 @@ const VectorSearch = () => {
             value={selectedCollection}
             value={selectedCollection}
             onChange={(e: { target: { value: unknown } }) => {
             onChange={(e: { target: { value: unknown } }) => {
               const collection = e.target.value;
               const collection = e.target.value;
+
               setSelectedCollection(collection as string);
               setSelectedCollection(collection as string);
               // every time selected collection changed, reset field
               // every time selected collection changed, reset field
               setSelectedField('');
               setSelectedField('');

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

@@ -61,3 +61,17 @@ fieldset {
 .dialog-content::first-letter {
 .dialog-content::first-letter {
   text-transform: uppercase;
   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 {
   interface Palette {
     milvusBlue: Palette['primary'];
     milvusBlue: Palette['primary'];
     milvusGrey: Palette['primary'];
     milvusGrey: Palette['primary'];
+    milvusDark: Palette['primary'];
   }
   }
   interface PaletteOptions {
   interface PaletteOptions {
     milvusBlue: PaletteOptions['primary'];
     milvusBlue: PaletteOptions['primary'];
     milvusGrey: PaletteOptions['primary'];
     milvusGrey: PaletteOptions['primary'];
+    milvusDark: PaletteOptions['primary'];
   }
   }
 }
 }
 
 
@@ -55,6 +57,9 @@ const commonThemes = {
       dark: '#82838e',
       dark: '#82838e',
       contrastText: '#f8f8fc',
       contrastText: '#f8f8fc',
     },
     },
+    milvusDark: {
+      main: '#010e29',
+    },
   },
   },
   breakpoints: {
   breakpoints: {
     values: {
     values: {

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

@@ -91,6 +91,20 @@ export const getKeyValuePairFromObj = (
   return pairs;
   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 = (
 export const getKeyValueListFromJsonString = (
   json: string
   json: string
 ): { key: string; value: 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;
+};

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

@@ -0,0 +1,27 @@
+import { getObjFromKeyValuePair } from '../Format';
+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 obj = getObjFromKeyValuePair(extraParams);
+  const index = {
+    ...obj,
+    params: parseValue(obj.params),
+  };
+  const pyCode = `from pymilvus_orm import Collection
+
+collection = Collection('${collectionName}')
+index = ${JSON.stringify(index, replacer, 4)}
+collection.create_index(${fieldName}, index)`;
+
+  return pyCode;
+};

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

@@ -0,0 +1,10 @@
+export interface KeyValuePairs {
+  key: string;
+  value: string;
+}
+
+export interface CreateIndexCodeParam {
+  collectionName: string;
+  fieldName: string;
+  extraParams: KeyValuePairs[];
+}

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

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

+ 157 - 0
client/yarn.lock

@@ -1869,6 +1869,13 @@
   dependencies:
   dependencies:
     "@types/node" "*"
     "@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@*":
 "@types/history@*":
   version "4.7.8"
   version "4.7.8"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
@@ -2001,6 +2008,13 @@
     "@types/history" "*"
     "@types/history" "*"
     "@types/react" "*"
     "@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":
 "@types/react-test-renderer@>=16.9.0":
   version "17.0.1"
   version "17.0.1"
   resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b"
   resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b"
@@ -2074,6 +2088,11 @@
   dependencies:
   dependencies:
     source-map "^0.6.1"
     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@*":
 "@types/webpack-sources@*":
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.1.0.tgz#8882b0bd62d1e0ce62f183d0d01b72e6e82e8c10"
   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"
   resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
   integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
   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:
 check-types@^11.1.1:
   version "11.1.2"
   version "11.1.2"
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
@@ -3594,6 +3628,11 @@ combined-stream@^1.0.8:
   dependencies:
   dependencies:
     delayed-stream "~1.0.0"
     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:
 commander@^2.20.0:
   version "2.20.3"
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -5144,6 +5183,13 @@ fastq@^1.6.0:
   dependencies:
   dependencies:
     reusify "^1.0.4"
     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:
 faye-websocket@^0.11.3:
   version "0.11.4"
   version "0.11.4"
   resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
   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"
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
     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:
 forwarded@0.2.0:
   version "0.2.0"
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
   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"
     inherits "^2.0.3"
     minimalistic-assert "^1.0.1"
     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:
 he@^1.2.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   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"
   resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa"
   integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==
   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:
 history@^4.9.0:
   version "4.10.1"
   version "4.10.1"
   resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
   resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
@@ -6100,6 +6172,19 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
   dependencies:
     kind-of "^6.0.0"
     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:
 is-arguments@^1.0.4:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
   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"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5"
   integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==
   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:
 is-descriptor@^0.1.0:
   version "0.1.6"
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
   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:
   dependencies:
     is-extglob "^2.1.1"
     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:
 is-in-browser@^1.0.2, is-in-browser@^1.1.3:
   version "1.1.3"
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
   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:
   dependencies:
     tslib "^2.0.3"
     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:
 lru-cache@^5.1.1:
   version "5.1.1"
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
   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"
     pbkdf2 "^3.0.3"
     safe-buffer "^5.1.1"
     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:
 parse-json@^4.0.0:
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
   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"
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
   integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
   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:
 pretty-bytes@^5.3.0:
   version "5.6.0"
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
   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"
     ansi-styles "^4.0.0"
     react-is "^17.0.1"
     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:
 process-nextick-args@~2.0.0:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
   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"
     object-assign "^4.1.1"
     react-is "^16.8.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:
 proxy-addr@~2.0.5:
   version "2.0.7"
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -9543,6 +9675,17 @@ react-scripts@4.0.3:
   optionalDependencies:
   optionalDependencies:
     fsevents "^2.1.3"
     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:
 react-transition-group@^4.4.0:
   version "4.4.2"
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
   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"
     indent-string "^4.0.0"
     strip-indent "^3.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:
 regenerate-unicode-properties@^8.2.0:
   version "8.2.0"
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
   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"
   resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
   integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
   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:
 spdx-correct@^3.0.0:
   version "3.1.1"
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"