Browse Source

Merge pull request #4 from zilliztech/uxd-623

[UXD-623] add overview page ui
nameczz 4 years ago
parent
commit
c1ae58c4f4
52 changed files with 917 additions and 178 deletions
  1. 26 21
      client/public/index.html
  2. 7 1
      client/src/App.tsx
  3. 6 0
      client/src/assets/icons/collecion.svg
  4. 3 0
      client/src/assets/icons/console.svg
  5. 5 0
      client/src/assets/icons/info.svg
  6. 4 0
      client/src/assets/icons/overview.svg
  7. 17 0
      client/src/assets/icons/release.svg
  8. 6 6
      client/src/components/__test__/status/Status.spec.tsx
  9. 33 0
      client/src/components/cards/EmptyCard.tsx
  10. 7 0
      client/src/components/cards/Types.ts
  11. 1 5
      client/src/components/customInput/SearchInput.tsx
  12. 2 1
      client/src/components/customToolTip/CustomToolTip.tsx
  13. 31 1
      client/src/components/icons/Icons.tsx
  14. 11 1
      client/src/components/icons/Types.ts
  15. 86 4
      client/src/components/layout/Header.tsx
  16. 44 13
      client/src/components/layout/Layout.tsx
  17. 46 60
      client/src/components/menu/NavMenu.tsx
  18. 3 1
      client/src/components/menu/Types.ts
  19. 7 7
      client/src/components/status/Status.tsx
  20. 2 2
      client/src/components/status/Types.ts
  21. 22 0
      client/src/context/Auth.tsx
  22. 24 0
      client/src/context/Navigation.tsx
  23. 15 1
      client/src/context/Types.ts
  24. 59 0
      client/src/hooks/Navigation.ts
  25. 0 1
      client/src/http/Axios.ts
  26. 1 0
      client/src/i18n/cn/button.ts
  27. 7 0
      client/src/i18n/cn/collection.ts
  28. 3 2
      client/src/i18n/cn/common.ts
  29. 7 0
      client/src/i18n/cn/nav.ts
  30. 8 0
      client/src/i18n/cn/overview.ts
  31. 1 0
      client/src/i18n/en/button.ts
  32. 7 0
      client/src/i18n/en/collection.ts
  33. 3 2
      client/src/i18n/en/common.ts
  34. 7 0
      client/src/i18n/en/nav.ts
  35. 8 0
      client/src/i18n/en/overview.ts
  36. 12 0
      client/src/i18n/index.ts
  37. 2 0
      client/src/index.css
  38. 9 0
      client/src/pages/collections/Collections.tsx
  39. 11 6
      client/src/pages/connect/Connect.tsx
  40. 1 5
      client/src/pages/connect/ConnectContainer.tsx
  41. 9 0
      client/src/pages/console/Console.tsx
  42. 118 18
      client/src/pages/overview/Overview.tsx
  43. 97 0
      client/src/pages/overview/collectionCard/CollectionCard.tsx
  44. 13 0
      client/src/pages/overview/collectionCard/Types.ts
  45. 52 0
      client/src/pages/overview/statisticsCard/StatisticsCard.tsx
  46. 10 0
      client/src/pages/overview/statisticsCard/Types.ts
  47. 12 0
      client/src/router/Config.ts
  48. 21 1
      client/src/router/Router.tsx
  49. 9 15
      client/src/router/Types.ts
  50. 19 0
      client/src/styles/common.css
  51. 1 2
      client/src/utils/Common.ts
  52. 2 2
      client/src/utils/Status.ts

+ 26 - 21
client/public/index.html

@@ -1,21 +1,27 @@
 <!DOCTYPE html>
 <html lang="en">
-
-<head>
-  <meta charset="utf-8" />
-  <link rel="icon" href="%PUBLIC_URL%/favicon.png" />
-  <meta name="viewport" content="width=device-width, initial-scale=1" />
-  <meta name="theme-color" content="#000000" />
-  <meta name="description" content="Web site created using create-react-app" />
-  <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
-  <!--
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.png" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
       manifest.json provides metadata used when your web app is installed on a
       user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
     -->
-  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
-  <script src="%PUBLIC_URL%/env-config.js"></script>
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap"
+      rel="stylesheet"
+    />
+    <script src="%PUBLIC_URL%/env-config.js"></script>
 
-  <!--
+    <!--
       Notice the use of %PUBLIC_URL% in the tags above.
       It will be replaced with the URL of the `public` folder during the build.
       Only files inside the `public` folder can be referenced from the HTML.
@@ -24,13 +30,13 @@
       work correctly both with client-side routing and a non-root public URL.
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
-  <title>Milvus Admin</title>
-</head>
+    <title>Milvus Admin</title>
+  </head>
 
-<body>
-  <noscript>You need to enable JavaScript to run this app.</noscript>
-  <div id="root"></div>
-  <!--
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
       This HTML file is a template.
       If you open it directly in the browser, you will see an empty page.
 
@@ -40,6 +46,5 @@
       To begin the development, run `npm start` or `yarn start`.
       To create a production bundle, use `npm run build` or `yarn build`.
     -->
-</body>
-
-</html>
+  </body>
+</html>

+ 7 - 1
client/src/App.tsx

@@ -1,10 +1,16 @@
 import Router from './router/Router';
 import { RootProvider } from './context/Root';
+import { NavProvider } from './context/Navigation';
+import { AuthProvider } from './context/Auth';
 
 function App() {
   return (
     <RootProvider>
-      <Router></Router>
+      <AuthProvider>
+        <NavProvider>
+          <Router></Router>
+        </NavProvider>
+      </AuthProvider>
     </RootProvider>
   );
 }

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

@@ -0,0 +1,6 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.75 7.8333L6.25 3.5083" stroke="#82838E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M17.5 13.3333V6.66663C17.4997 6.37435 17.4225 6.0873 17.2763 5.83426C17.13 5.58122 16.9198 5.37109 16.6667 5.22496L10.8333 1.89163C10.58 1.74535 10.2926 1.66833 10 1.66833C9.70744 1.66833 9.42003 1.74535 9.16667 1.89163L3.33333 5.22496C3.08022 5.37109 2.86998 5.58122 2.72372 5.83426C2.57745 6.0873 2.5003 6.37435 2.5 6.66663V13.3333C2.5003 13.6256 2.57745 13.9126 2.72372 14.1657C2.86998 14.4187 3.08022 14.6288 3.33333 14.775L9.16667 18.1083C9.42003 18.2546 9.70744 18.3316 10 18.3316C10.2926 18.3316 10.58 18.2546 10.8333 18.1083L16.6667 14.775C16.9198 14.6288 17.13 14.4187 17.2763 14.1657C17.4225 13.9126 17.4997 13.6256 17.5 13.3333Z" stroke="#82838E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.72461 5.80005L9.99961 10.0084L17.2746 5.80005" stroke="#82838E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 18.4V10" stroke="#82838E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

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

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.667 15.8333V5.83333H3.33366V15.8333H16.667ZM16.667 2.5C17.109 2.5 17.5329 2.67559 17.8455 2.98816C18.1581 3.30072 18.3337 3.72464 18.3337 4.16667V15.8333C18.3337 16.2754 18.1581 16.6993 17.8455 17.0118C17.5329 17.3244 17.109 17.5 16.667 17.5H3.33366C2.89163 17.5 2.46771 17.3244 2.15515 17.0118C1.84259 16.6993 1.66699 16.2754 1.66699 15.8333V4.16667C1.66699 3.72464 1.84259 3.30072 2.15515 2.98816C2.46771 2.67559 2.89163 2.5 3.33366 2.5H16.667ZM10.8337 14.1667V12.5H15.0003V14.1667H10.8337ZM7.98366 10.8333L4.64199 7.5H7.00033L9.75033 10.25C10.0753 10.575 10.0753 11.1083 9.75033 11.4333L7.01699 14.1667H4.65866L7.98366 10.8333Z" fill="#82838E"/>
+</svg>

+ 5 - 0
client/src/assets/icons/info.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99996 1.99999C4.68625 1.99999 1.99996 4.68628 1.99996 7.99999C1.99996 11.3137 4.68625 14 7.99996 14C11.3137 14 14 11.3137 14 7.99999C14 4.68628 11.3137 1.99999 7.99996 1.99999ZM0.666626 7.99999C0.666626 3.9499 3.94987 0.666656 7.99996 0.666656C12.05 0.666656 15.3333 3.9499 15.3333 7.99999C15.3333 12.0501 12.05 15.3333 7.99996 15.3333C3.94987 15.3333 0.666626 12.0501 0.666626 7.99999Z" fill="#06AFF2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00004 7.33334C8.36823 7.33334 8.66671 7.63182 8.66671 8.00001V10.6667C8.66671 11.0349 8.36823 11.3333 8.00004 11.3333C7.63185 11.3333 7.33337 11.0349 7.33337 10.6667V8.00001C7.33337 7.63182 7.63185 7.33334 8.00004 7.33334Z" fill="#06AFF2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.33337 5.33332C7.33337 4.96513 7.63185 4.66666 8.00004 4.66666H8.00671C8.3749 4.66666 8.67337 4.96513 8.67337 5.33332C8.67337 5.70151 8.3749 5.99999 8.00671 5.99999H8.00004C7.63185 5.99999 7.33337 5.70151 7.33337 5.33332Z" fill="#06AFF2"/>
+</svg>

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

@@ -0,0 +1,4 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.5 7.49996L10 1.66663L17.5 7.49996V16.6666C17.5 17.1087 17.3244 17.5326 17.0118 17.8451C16.6993 18.1577 16.2754 18.3333 15.8333 18.3333H4.16667C3.72464 18.3333 3.30072 18.1577 2.98816 17.8451C2.67559 17.5326 2.5 17.1087 2.5 16.6666V7.49996Z" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 18.3333V10H12.5V18.3333" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 17 - 0
client/src/assets/icons/release.svg

@@ -0,0 +1,17 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.74339 13.2866C1.57182 12.115 1.57182 10.2155 2.74339 9.04396L4.39331 7.39405C4.65366 7.1337 5.07577 7.1337 5.33612 7.39405C5.59647 7.6544 5.59647 8.07651 5.33612 8.33686L3.6862 9.98677C3.03533 10.6376 3.03533 11.6929 3.6862 12.3438C4.33707 12.9947 5.39235 12.9947 6.04322 12.3438L7.69314 10.6939C7.95349 10.4335 8.3756 10.4335 8.63595 10.6939C8.8963 10.9542 8.8963 11.3763 8.63595 11.6367L6.98603 13.2866C5.81446 14.4582 3.91496 14.4582 2.74339 13.2866Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6428 3.3871C13.8144 4.55868 13.8144 6.45817 12.6428 7.62974L10.9929 9.27966C10.7326 9.54001 10.3105 9.54001 10.0501 9.27966C9.78977 9.01931 9.78977 8.5972 10.0501 8.33685L11.7 6.68693C12.3509 6.03606 12.3509 4.98079 11.7 4.32991C11.0492 3.67904 9.99388 3.67904 9.34301 4.32991L7.69309 5.97983C7.43274 6.24018 7.01063 6.24018 6.75028 5.97983C6.48993 5.71948 6.48993 5.29737 6.75028 5.03702L8.4002 3.3871C9.57177 2.21553 11.4713 2.21553 12.6428 3.3871Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1.29944 6.37793C1.29944 6.10179 1.5233 5.87793 1.79944 5.87793H3.05963C3.33577 5.87793 3.55963 6.10179 3.55963 6.37793C3.55963 6.65407 3.33577 6.87793 3.05963 6.87793H1.79944C1.5233 6.87793 1.29944 6.65407 1.29944 6.37793Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5.73157 1.98645C5.45543 1.98645 5.23157 2.21031 5.23157 2.48645V3.74645C5.23157 4.02259 5.45543 4.24645 5.73157 4.24645C6.00771 4.24645 6.23157 4.02259 6.23157 3.74645V2.48645C6.23157 2.21031 6.00771 1.98645 5.73157 1.98645Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.51681 3.06168C2.71207 2.86642 3.02865 2.86642 3.22391 3.06168L4.64773 4.4855C4.843 4.68076 4.843 4.99735 4.64773 5.19261C4.45247 5.38787 4.13589 5.38787 3.94063 5.19261L2.51681 3.76879C2.32155 3.57353 2.32155 3.25695 2.51681 3.06168Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.91736 14.2424C10.1935 14.2424 10.4174 14.0186 10.4174 13.7424V12.4822C10.4174 12.2061 10.1935 11.9822 9.91736 11.9822C9.64122 11.9822 9.41736 12.2061 9.41736 12.4822V13.7424C9.41736 14.0186 9.64122 14.2424 9.91736 14.2424Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3088 9.37524C14.3088 9.65139 14.085 9.87524 13.8088 9.87524H12.5487C12.2725 9.87524 12.0487 9.65139 12.0487 9.37524C12.0487 9.0991 12.2725 8.87524 12.5487 8.87524H13.8088C14.085 8.87524 14.3088 9.0991 14.3088 9.37524Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2337 13.0252C13.429 12.8299 13.429 12.5133 13.2337 12.3181L11.8099 10.8943C11.6146 10.699 11.2981 10.699 11.1028 10.8943C10.9075 11.0895 10.9075 11.4061 11.1028 11.6014L12.5266 13.0252C12.7219 13.2204 13.0385 13.2204 13.2337 13.0252Z" fill="white"/>
+</g>
+<defs>
+<clipPath id="clip0">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

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

@@ -11,8 +11,8 @@ jest.mock('react-i18next', () => {
       return {
         t: name => {
           return {
-            creating: 'creating',
-            running: 'running',
+            loaded: 'loaded',
+            unloaded: 'unloaded',
             error: 'error',
           };
         },
@@ -41,15 +41,15 @@ describe('Test Status', () => {
 
   it('Test props status', () => {
     act(() => {
-      render(<Status status={StatusEnum.creating} />, container);
+      render(<Status status={StatusEnum.loaded} />, container);
     });
 
-    expect(container.querySelector('.label').textContent).toEqual('creating');
+    expect(container.querySelector('.label').textContent).toEqual('loaded');
 
     act(() => {
-      render(<Status status={StatusEnum.running} />, container);
+      render(<Status status={StatusEnum.unloaded} />, container);
     });
 
-    expect(container.querySelector('.label').textContent).toEqual('running');
+    expect(container.querySelector('.label').textContent).toEqual('unloaded');
   });
 });

+ 33 - 0
client/src/components/cards/EmptyCard.tsx

@@ -0,0 +1,33 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { FC } from 'react';
+import { EmptyCardProps } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    backgroundColor: '#fff',
+    flexDirection: 'column',
+  },
+  text: {
+    marginTop: theme.spacing(4),
+    fontSize: '36px',
+    lineHeight: '42px',
+    color: '#82838e',
+    fontWeight: 'bold',
+    letterSpacing: '-0.02em',
+  },
+}));
+
+const EmptyCard: FC<EmptyCardProps> = ({ icon, text, wrapperClass = '' }) => {
+  const classes = useStyles();
+
+  return (
+    <section
+      className={`flex-center card-wrapper ${classes.wrapper} ${wrapperClass}`}
+    >
+      {icon}
+      <Typography className={classes.text}>{text}</Typography>
+    </section>
+  );
+};
+
+export default EmptyCard;

+ 7 - 0
client/src/components/cards/Types.ts

@@ -0,0 +1,7 @@
+import { ReactElement } from 'react';
+
+export interface EmptyCardProps {
+  icon: ReactElement;
+  text: string;
+  wrapperClass?: string;
+}

+ 1 - 5
client/src/components/customInput/SearchInput.tsx

@@ -38,10 +38,6 @@ const useSearchStyles = makeStyles(theme => ({
     cursor: 'pointer',
   },
   iconWrapper: {
-    display: 'flex',
-    justifyContent: 'center',
-    alignItems: 'center',
-
     opacity: (props: any) => `${props.searched ? 1 : 0}`,
     transition: 'opacity 0.2s',
   },
@@ -94,7 +90,7 @@ const SearchInput: FC<SearchType> = props => {
           endAdornment: (
             <InputAdornment position="end">
               <span
-                className={classes.iconWrapper}
+                className={`flex-center ${classes.iconWrapper}`}
                 onClick={e => {
                   setSearchValue('');
                   inputRef.current.focus();

+ 2 - 1
client/src/components/customToolTip/CustomToolTip.tsx

@@ -20,8 +20,9 @@ const CustomToolTip: FC<CustomToolTipType> = props => {
       leaveDelay={leaveDelay}
       title={title}
       placement={placement}
+      arrow
     >
-      <span>{children}</span>
+      <span className="flex-center">{children}</span>
     </Tooltip>
   );
 };

+ 31 - 1
client/src/components/icons/Icons.tsx

@@ -12,9 +12,18 @@ import AppsIcon from '@material-ui/icons/Apps';
 import MoreVertIcon from '@material-ui/icons/MoreVert';
 import CancelIcon from '@material-ui/icons/Cancel';
 import CheckCircleIcon from '@material-ui/icons/CheckCircle';
-
+import ExpandLess from '@material-ui/icons/ExpandLess';
+import ExpandMore from '@material-ui/icons/ExpandMore';
+import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
+import ExitToAppIcon from '@material-ui/icons/ExitToApp';
+import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
 import { SvgIcon } from '@material-ui/core';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
+import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
+import { ReactComponent as CollectionIcon } from '../../assets/icons/collecion.svg';
+import { ReactComponent as ConsoleIcon } from '../../assets/icons/console.svg';
+import { ReactComponent as InfoIcon } from '../../assets/icons/info.svg';
+import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
@@ -29,9 +38,30 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   more: (props = {}) => <MoreVertIcon {...props} />,
   app: (props = {}) => <AppsIcon {...props} />,
   success: (props = {}) => <CheckCircleIcon {...props} />,
+  expandLess: (props = {}) => <ExpandLess {...props} />,
+  expandMore: (props = {}) => <ExpandMore {...props} />,
+  back: (props = {}) => <ArrowBackIosIcon {...props} />,
+  logout: (props = {}) => <ExitToAppIcon {...props} />,
+  rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
+
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />
   ),
+  navOverview: (props = {}) => (
+    <SvgIcon viewBox="0 0 20 20" component={OverviewIcon} {...props} />
+  ),
+  navCollection: (props = {}) => (
+    <SvgIcon viewBox="0 0 20 20" component={CollectionIcon} {...props} />
+  ),
+  navConsole: (props = {}) => (
+    <SvgIcon viewBox="0 0 20 20" component={ConsoleIcon} {...props} />
+  ),
+  info: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
+  ),
+  release: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={ReleaseIcon} {...props} />
+  ),
 };
 
 export default icons;

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

@@ -11,4 +11,14 @@ export type IconsType =
   | 'app'
   | 'more'
   | 'success'
-  | 'milvus';
+  | 'milvus'
+  | 'navOverview'
+  | 'navCollection'
+  | 'navConsole'
+  | 'expandLess'
+  | 'expandMore'
+  | 'back'
+  | 'logout'
+  | 'rightArrow'
+  | 'info'
+  | 'release';

+ 86 - 4
client/src/components/layout/Header.tsx

@@ -1,7 +1,11 @@
-import { FC } from 'react';
-import { makeStyles, Theme, createStyles } from '@material-ui/core';
+import { FC, useContext } from 'react';
+import { makeStyles, Theme, createStyles, Typography } from '@material-ui/core';
 import { HeaderType } from './Types';
-// import { useTranslation } from 'react-i18next';
+import { navContext } from '../../context/Navigation';
+import icons from '../icons/Icons';
+import { useHistory } from 'react-router-dom';
+import { authContext } from '../../context/Auth';
+import { useTranslation } from 'react-i18next';
 
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
@@ -11,13 +15,91 @@ const useStyles = makeStyles((theme: Theme) =>
       color: theme.palette.common.black,
       marginRight: theme.spacing(5),
     },
+    contentWrapper: {
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      paddingTop: theme.spacing(3),
+      paddingLeft: theme.spacing(6),
+      flex: 1,
+    },
+    navigation: {
+      display: 'flex',
+      alignItems: 'center',
+    },
+    icon: {
+      color: theme.palette.primary.main,
+      cursor: 'pointer',
+    },
+    addressWrapper: {
+      display: 'flex',
+      alignItems: 'center',
+
+      '& .text': {
+        marginRight: theme.spacing(3),
+
+        '& .address': {
+          fontSize: '14px',
+          lineHeight: '20px',
+          color: '#545454',
+        },
+
+        '& .status': {
+          fontSize: '12px',
+          lineHeight: '16px',
+          color: '#1ba954',
+          textTransform: 'capitalize',
+        },
+      },
+    },
   })
 );
 
 const Header: FC<HeaderType> = props => {
   const classes = useStyles();
+  const { navInfo } = useContext(navContext);
+  const { address, setIsAuth, setAddress } = useContext(authContext);
+  const history = useHistory();
+  const { t } = useTranslation();
+  const statusTrans: { [key in string]: string } = t('status');
+  const BackIcon = icons.back;
+  const LogoutIcon = icons.logout;
+
+  const handleBack = (path: string) => {
+    history.push(path);
+  };
+
+  const handleLogout = () => {
+    setAddress('');
+    setIsAuth(false);
+  };
+
+  return (
+    <header className={classes.header}>
+      <div className={classes.contentWrapper}>
+        <div className={classes.navigation}>
+          {navInfo.backPath !== '' && (
+            <BackIcon
+              classes={{ root: classes.icon }}
+              onClick={() => handleBack(navInfo.backPath)}
+            />
+          )}
+
+          <Typography variant="h4" color="textPrimary">
+            {navInfo.navTitle}
+          </Typography>
+        </div>
 
-  return <header className={classes.header}>header</header>;
+        <div className={classes.addressWrapper}>
+          <div className="text">
+            <Typography className="address">{address}</Typography>
+            <Typography className="status">{statusTrans.running}</Typography>
+          </div>
+          <LogoutIcon classes={{ root: classes.icon }} onClick={handleLogout} />
+        </div>
+      </div>
+    </header>
+  );
 };
 
 export default Header;

+ 44 - 13
client/src/components/layout/Layout.tsx

@@ -3,14 +3,17 @@ import Header from './Header';
 import { makeStyles, Theme, createStyles } from '@material-ui/core';
 import NavMenu from '../menu/NavMenu';
 import { NavMenuItem } from '../menu/Types';
-// import { useHistory } from 'react-router';
-// import { useTranslation } from 'react-i18next';
+import { useContext } from 'react';
+import icons from '../icons/Icons';
+import { useTranslation } from 'react-i18next';
+import { useHistory } from 'react-router-dom';
+import { authContext } from '../../context/Auth';
 
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
     root: {
       minHeight: '100vh',
-      backgroundColor: (props: any) => props.backgroundColor,
+      backgroundColor: '#f5f5f5',
     },
     content: {
       display: 'flex',
@@ -19,20 +22,47 @@ const useStyles = makeStyles((theme: Theme) =>
       flex: 1,
       display: 'flex',
       flexDirection: 'column',
-      height: `100vh`,
+      height: '100vh',
       overflowY: 'scroll',
     },
+    activeConsole: {
+      '& path': {
+        fill: theme.palette.primary.main,
+      },
+    },
+    normalConsole: {
+      '& path': {
+        fill: '#82838e',
+      },
+    },
   })
 );
 
 const Layout = (props: any) => {
-  const path = window.location.hash.slice(2);
-  const greyPaths = ['', 'billing'];
-  const bgColor = greyPaths.includes(path) ? '#f5f5f5' : '#fff';
-  const classes = useStyles({ backgroundColor: bgColor });
+  const history = useHistory();
+  const { isAuth } = useContext(authContext);
+  const { t } = useTranslation('nav');
+  const classes = useStyles();
 
-  const data: NavMenuItem[] = [];
-  const isAuth = false;
+  const menuItems: NavMenuItem[] = [
+    {
+      icon: icons.navOverview,
+      label: t('overview'),
+      onClick: () => history.push('/'),
+    },
+    {
+      icon: icons.navCollection,
+      label: t('collection'),
+      onClick: () => history.push('/collections'),
+    },
+    {
+      icon: icons.navConsole,
+      label: t('console'),
+      onClick: () => history.push('/console'),
+      iconActiveClass: classes.activeConsole,
+      iconNormalClass: classes.normalConsole,
+    },
+  ];
 
   return (
     <div className={classes.root}>
@@ -41,9 +71,10 @@ const Layout = (props: any) => {
           {isAuth && (
             <NavMenu
               width="200px"
-              data={data}
-              defaultActive="Lock"
-              defaultOpen={{ security: true }}
+              data={menuItems}
+              defaultActive={t('overview')}
+              // used for nested child menu
+              defaultOpen={{ [t('overview')]: true }}
             />
           )}
 

+ 46 - 60
client/src/components/menu/NavMenu.tsx

@@ -1,16 +1,14 @@
-import { useState, FC, useEffect } from 'react';
+import { useState, FC } from 'react';
 import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
 import List from '@material-ui/core/List';
 import ListItem from '@material-ui/core/ListItem';
 import ListItemIcon from '@material-ui/core/ListItemIcon';
 import ListItemText from '@material-ui/core/ListItemText';
 import Collapse from '@material-ui/core/Collapse';
-import ExpandLess from '@material-ui/icons/ExpandLess';
-import ExpandMore from '@material-ui/icons/ExpandMore';
 import { NavMenuItem, NavMenuType } from './Types';
-import { useTranslation } from 'react-i18next';
-import { useLocation } from 'react-router';
 import icons from '../icons/Icons';
+import { useTranslation } from 'react-i18next';
+import Typography from '@material-ui/core/Typography';
 
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
@@ -32,39 +30,26 @@ const useStyles = makeStyles((theme: Theme) =>
 
       width: 'initial',
       color: '#82838e',
+    },
+    itemIcon: {
+      minWidth: '20px',
+      marginRight: theme.spacing(1),
 
-      '& svg': {
-        width: '20px',
-        height: '20px',
+      '& .icon': {
+        fill: 'transparent',
 
         '& path': {
           stroke: '#82838e',
         },
-
-        '& .st0': {
-          fill: '#82838e',
-        },
       },
     },
-    itemIcon: {
-      minWidth: '20px',
-      marginRight: theme.spacing(1),
-    },
     active: {
       color: theme.palette.primary.main,
       borderRight: `2px solid ${theme.palette.primary.main}`,
 
-      '& svg': {
+      '& .icon': {
         '& path': {
           stroke: theme.palette.primary.main,
-
-          transition: 'all 0.2s',
-        },
-
-        '& .st0': {
-          fill: theme.palette.primary.main,
-
-          transition: 'all 0.2s',
         },
       },
     },
@@ -75,21 +60,21 @@ const useStyles = makeStyles((theme: Theme) =>
       justifyContent: 'center',
       alignItems: 'center',
 
-      marginTop: '30px',
-
-      marginBottom: '65px',
-    },
-    logo: {
-      width: '150px',
-    },
+      marginTop: theme.spacing(3),
 
-    feedback: {
-      color: theme.palette.primary.main,
+      marginBottom: theme.spacing(8),
 
-      '&:hover': {
-        backgroundColor: '#fff',
+      '& .title': {
+        margin: 0,
+        fontSize: '16px',
+        lineHeight: '19px',
+        letterSpacing: '0.15px',
+        color: '#323232',
       },
     },
+    logo: {
+      marginRight: theme.spacing(1),
+    },
   })
 );
 
@@ -98,7 +83,12 @@ const NavMenu: FC<NavMenuType> = props => {
   const classes = useStyles({ width });
   const [open, setOpen] = useState<{ [x: string]: boolean }>(defaultOpen);
   const [active, setActive] = useState<string>(defaultActive);
-  const location = useLocation();
+
+  const { t } = useTranslation();
+  const milvusTrans: { [key in string]: string } = t('milvus');
+
+  const ExpandLess = icons.expandLess;
+  const ExpandMore = icons.expandMore;
 
   const handleClick = (label: string) => {
     setOpen(v => ({
@@ -107,31 +97,26 @@ const NavMenu: FC<NavMenuType> = props => {
     }));
   };
 
-  const { t } = useTranslation();
-  // const navTrans: { [key in string]: string | object } = t('nav');
-
-  // useEffect(() => {
-  //   const activeLabel = location.pathname.includes('queries')
-  //     ? (navTrans.query as string)
-  //     : (navTrans.database as string);
-  //   setActive(activeLabel);
-  // }, [location.pathname, navTrans.query, navTrans.database]);
-
   const NestList = (props: { data: NavMenuItem[]; className?: string }) => {
     const { className, data } = props;
     return (
       <>
-        {data.map((v: any) => {
+        {data.map((v: NavMenuItem) => {
           const IconComponent = v.icon;
+          const isActive = active === v.label;
+          const iconClass =
+            v.iconActiveClass && v.iconNormalClass
+              ? isActive
+                ? v.iconActiveClass
+                : v.iconNormalClass
+              : 'icon';
           if (v.children) {
             return (
               <div key={v.label}>
                 <ListItem button onClick={() => handleClick(v.label)}>
-                  {v.icon && (
-                    <ListItemIcon>
-                      <IconComponent />
-                    </ListItemIcon>
-                  )}
+                  <ListItemIcon>
+                    <IconComponent classes={{ root: iconClass }} />
+                  </ListItemIcon>
 
                   <ListItemText primary={v.label} />
                   {open[v.label] ? <ExpandLess /> : <ExpandMore />}
@@ -149,18 +134,16 @@ const NavMenu: FC<NavMenuType> = props => {
               button
               key={v.label}
               className={`${className || ''} ${classes.item} ${
-                active === v.label ? classes.active : ''
+                isActive ? classes.active : ''
               }`}
               onClick={() => {
                 setActive(v.label);
                 v.onClick && v.onClick();
               }}
             >
-              {v.icon && (
-                <ListItemIcon className={classes.itemIcon}>
-                  <IconComponent />
-                </ListItemIcon>
-              )}
+              <ListItemIcon className={classes.itemIcon}>
+                <IconComponent classes={{ root: iconClass }} />
+              </ListItemIcon>
 
               <ListItemText primary={v.label} />
             </ListItem>
@@ -177,9 +160,12 @@ const NavMenu: FC<NavMenuType> = props => {
       <div>
         <div className={classes.logoWrapper}>
           <Logo classes={{ root: classes.logo }} />
+          <Typography variant="h3" className="title">
+            {milvusTrans.admin}
+          </Typography>
         </div>
 
-        {/* <NestList data={data} /> */}
+        <NestList data={data} />
       </div>
     </List>
   );

+ 3 - 1
client/src/components/menu/Types.ts

@@ -12,7 +12,9 @@ export type NavMenuItem = {
   icon: (
     props?: any
   ) => React.ReactElement<any, string | React.JSXElementConstructor<any>>;
-  label: String;
+  iconActiveClass?: string;
+  iconNormalClass?: string;
+  label: string;
   onClick?: () => void;
   children?: NavMenuItem[];
 };

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

@@ -10,8 +10,8 @@ const useStyles = makeStyles((theme: Theme) =>
       alignItems: 'center',
     },
     label: {
-      fontWeight: 'bold',
-      textTransform: 'uppercase',
+      color: '#82838e',
+      textTransform: 'capitalize',
     },
     circle: {
       backgroundColor: (props: any) => props.color,
@@ -45,15 +45,15 @@ const Status: FC<StatusType> = props => {
   const statusTrans: { [key in string]: string } = t('status');
   const { label, color } = useMemo(() => {
     switch (status) {
-      case StatusEnum.creating:
+      case StatusEnum.unloaded:
         return {
-          label: statusTrans.creating,
+          label: statusTrans.unloaded,
           color: '#06aff2',
         };
 
-      case StatusEnum.running:
+      case StatusEnum.loaded:
         return {
-          label: statusTrans.running,
+          label: statusTrans.loaded,
           color: '#06f3af',
         };
       case StatusEnum.error:
@@ -76,7 +76,7 @@ const Status: FC<StatusType> = props => {
     <div className={classes.root}>
       <div
         className={`${classes.circle} ${
-          status === StatusEnum.creating ? classes.flash : ''
+          status === StatusEnum.unloaded ? classes.flash : ''
         }`}
       ></div>
       <Typography variant="body2" className={classes.label}>

+ 2 - 2
client/src/components/status/Types.ts

@@ -1,6 +1,6 @@
 export enum StatusEnum {
-  'creating',
-  'running',
+  'unloaded',
+  'loaded',
   'error',
 }
 export type StatusType = {

+ 22 - 0
client/src/context/Auth.tsx

@@ -0,0 +1,22 @@
+import { createContext, useState } from 'react';
+import { AuthContextType } from './Types';
+
+export const authContext = createContext<AuthContextType>({
+  isAuth: false,
+  setIsAuth: () => {},
+  address: '',
+  setAddress: () => {},
+});
+
+const { Provider } = authContext;
+
+export const AuthProvider = (props: { children: React.ReactNode }) => {
+  const [isAuth, setIsAuth] = useState<boolean>(false);
+  const [address, setAddress] = useState<string>('');
+
+  return (
+    <Provider value={{ isAuth, setIsAuth, address, setAddress }}>
+      {props.children}
+    </Provider>
+  );
+};

+ 24 - 0
client/src/context/Navigation.tsx

@@ -0,0 +1,24 @@
+import { createContext, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { NavInfo } from '../router/Types';
+import { NavContextType } from './Types';
+
+export const navContext = createContext<NavContextType>({
+  navInfo: {
+    navTitle: '',
+    backPath: '',
+  },
+  setNavInfo: () => {},
+});
+
+const { Provider } = navContext;
+
+export const NavProvider = (props: { children: React.ReactNode }) => {
+  const { t } = useTranslation('nav');
+  const [navInfo, setNavInfo] = useState<NavInfo>({
+    navTitle: t('overview'),
+    backPath: '',
+  });
+
+  return <Provider value={{ navInfo, setNavInfo }}>{props.children}</Provider>;
+};

+ 15 - 1
client/src/context/Types.ts

@@ -1,4 +1,5 @@
-import { ReactElement } from 'react';
+import { Dispatch, ReactElement, SetStateAction } from 'react';
+import { NavInfo } from '../router/Types';
 
 export type RootContextType = {
   openSnackBar: OpenSnackBarType;
@@ -31,6 +32,7 @@ export type DialogType = {
     containerClass?: string;
   };
 };
+
 export type SnackBarType = {
   open: boolean;
   message: string | ReactElement;
@@ -49,3 +51,15 @@ export type OpenSnackBarType = (
     vertical: 'bottom' | 'top';
   }
 ) => void;
+
+export type AuthContextType = {
+  isAuth: boolean;
+  setIsAuth: Dispatch<SetStateAction<boolean>>;
+  address: string;
+  setAddress: Dispatch<SetStateAction<string>>;
+};
+
+export type NavContextType = {
+  navInfo: NavInfo;
+  setNavInfo: (param: NavInfo) => void;
+};

+ 59 - 0
client/src/hooks/Navigation.ts

@@ -0,0 +1,59 @@
+import { useContext, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useParams } from 'react-router';
+import { navContext } from '../context/Navigation';
+import { ALL_ROUTER_TYPES, NavInfo } from '../router/Types';
+
+export const useNavigationHook = (
+  type: ALL_ROUTER_TYPES,
+  extraParam?: {
+    collectionName: string;
+  }
+) => {
+  const { t } = useTranslation('nav');
+  const { setNavInfo } = useContext(navContext);
+
+  const { collectionId = '' } =
+    useParams<{
+      collectionId?: string;
+    }>();
+
+  useEffect(() => {
+    switch (type) {
+      case ALL_ROUTER_TYPES.OVERVIEW: {
+        const navInfo: NavInfo = {
+          navTitle: t('overview'),
+          backPath: '',
+        };
+        setNavInfo(navInfo);
+        break;
+      }
+      case ALL_ROUTER_TYPES.COLLECTIONS: {
+        const navInfo: NavInfo = {
+          navTitle: t('collection'),
+          backPath: '',
+        };
+        setNavInfo(navInfo);
+        break;
+      }
+      case ALL_ROUTER_TYPES.COLLECTION_DETAIL: {
+        const navInfo: NavInfo = {
+          navTitle: extraParam?.collectionName as string,
+          backPath: '/collections',
+        };
+        setNavInfo(navInfo);
+        break;
+      }
+      case ALL_ROUTER_TYPES.CONSOLE: {
+        const navInfo: NavInfo = {
+          navTitle: t('console'),
+          backPath: '',
+        };
+        setNavInfo(navInfo);
+        break;
+      }
+      default:
+        break;
+    }
+  }, [type, extraParam, t, setNavInfo, collectionId]);
+};

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

@@ -21,7 +21,6 @@ axiosInstance.interceptors.request.use(
     // Do something before request is sent
     const session = window.localStorage.getItem(SESSION);
 
-    // console.log('in----', token);
     session && (config.headers[SESSION] = session);
 
     return config;

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

@@ -8,6 +8,7 @@ const btnTrans = {
   connect: '连接',
   import: '导入',
   delete: '删除',
+  release: 'Release',
 };
 
 export default btnTrans;

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

@@ -0,0 +1,7 @@
+const collectionTrans = {
+  noLoadData: 'No Loaded Collection',
+  rowCount: 'Row Count',
+  tooltip: 'data in one row',
+};
+
+export default collectionTrans;

+ 3 - 2
client/src/i18n/cn/common.ts

@@ -4,9 +4,10 @@ const commonTrans = {
     address: 'Milvus Address',
   },
   status: {
-    creating: 'creating',
-    running: 'running',
+    loaded: 'loaded',
+    unloaded: 'unloaded',
     error: 'error',
+    running: 'running',
   },
   grid: {
     action: 'action',

+ 7 - 0
client/src/i18n/cn/nav.ts

@@ -0,0 +1,7 @@
+const navTrans = {
+  overview: 'Overview',
+  collection: 'Collection',
+  console: 'Search Console',
+};
+
+export default navTrans;

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

@@ -0,0 +1,8 @@
+const overviewTrans = {
+  load: 'Loaded Collections',
+  all: 'All Collections',
+  data: 'Data',
+  rows: '{{number}} Rows',
+};
+
+export default overviewTrans;

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

@@ -9,6 +9,7 @@ const btnTrans = {
   connect: 'Connect',
   import: 'Import',
   delete: 'Delete',
+  release: 'Release',
 };
 
 export default btnTrans;

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

@@ -0,0 +1,7 @@
+const collectionTrans = {
+  noLoadData: 'No Loaded Collection',
+  rowCount: 'Row Count',
+  tooltip: 'data in one row',
+};
+
+export default collectionTrans;

+ 3 - 2
client/src/i18n/en/common.ts

@@ -4,9 +4,10 @@ const commonTrans = {
     address: 'Milvus Address',
   },
   status: {
-    creating: 'creating',
-    running: 'running',
+    loaded: 'loaded',
+    unloaded: 'unloaded',
     error: 'error',
+    running: 'running',
   },
   grid: {
     action: 'action',

+ 7 - 0
client/src/i18n/en/nav.ts

@@ -0,0 +1,7 @@
+const navTrans = {
+  overview: 'Overview',
+  collection: 'Collection',
+  console: 'Search Console',
+};
+
+export default navTrans;

+ 8 - 0
client/src/i18n/en/overview.ts

@@ -0,0 +1,8 @@
+const overviewTrans = {
+  load: 'Loaded Collections',
+  all: 'All Collections',
+  data: 'Data',
+  rows: '{{number}} Rows',
+};
+
+export default overviewTrans;

+ 12 - 0
client/src/i18n/index.ts

@@ -7,17 +7,29 @@ import buttonEn from './en/button';
 import buttonCn from './cn/button';
 import warningCn from './cn/warning';
 import warningEn from './en/warning';
+import navCn from './cn/nav';
+import navEn from './en/nav';
+import overviewCn from './cn/overview';
+import overviewEn from './en/overview';
+import collectionCn from './cn/collection';
+import collectionEn from './en/collection';
 
 export const resources = {
   cn: {
     translation: commonCn,
     btn: buttonCn,
     warning: warningCn,
+    nav: navCn,
+    overview: overviewCn,
+    collection: collectionCn,
   },
   en: {
     translation: commonEn,
     btn: buttonEn,
     warning: warningEn,
+    nav: navEn,
+    overview: overviewEn,
+    collection: collectionEn,
   },
 };
 

+ 2 - 0
client/src/index.css

@@ -1,3 +1,5 @@
+@import './styles/common.css';
+
 body {
   margin: 0;
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

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

@@ -0,0 +1,9 @@
+import { useNavigationHook } from '../../hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+
+const Collections = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
+  return <section>collection</section>;
+};
+
+export default Collections;

+ 11 - 6
client/src/pages/connect/Connect.tsx

@@ -1,14 +1,15 @@
-import { Theme, makeStyles } from '@material-ui/core';
+import { Theme, makeStyles, Typography } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { ITextfieldConfig } from '../../components/customInput/Types';
 import icons from '../../components/icons/Icons';
 import ConnectContainer from './ConnectContainer';
 import CustomInput from '../../components/customInput/CustomInput';
-import { useMemo, useState } from 'react';
+import { useContext, useMemo, useState } from 'react';
 import { formatForm } from '../../utils/Form';
 import { useFormValidation } from '../../hooks/Form';
 import CustomButton from '../../components/customButton/CustomButton';
 import { useHistory } from 'react-router-dom';
+import { authContext } from '../../context/Auth';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -23,9 +24,10 @@ const useStyles = makeStyles((theme: Theme) => ({
     padding: theme.spacing(3),
     margin: '0 auto',
 
-    '& h2': {
+    '& .title': {
       margin: 0,
       color: '#323232',
+      fontWeight: 'bold',
     },
   },
   logo: {
@@ -40,7 +42,7 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 const Connect = () => {
   const history = useHistory();
-
+  const { setIsAuth, setAddress } = useContext(authContext);
   const classes = useStyles();
   const { t } = useTranslation();
   const { t: warningTrans } = useTranslation('warning');
@@ -63,7 +65,8 @@ const Connect = () => {
   };
 
   const handleConnect = () => {
-    console.log('connect address', form.address);
+    setIsAuth(true);
+    setAddress(form.address);
     history.push('/');
   };
 
@@ -88,7 +91,9 @@ const Connect = () => {
       <section className={classes.wrapper}>
         <div className={classes.titleWrapper}>
           <Logo classes={{ root: classes.logo }} />
-          <h2>{milvusTrans.admin}</h2>
+          <Typography variant="h2" className="title">
+            {milvusTrans.admin}
+          </Typography>
         </div>
         <CustomInput
           type="text"

+ 1 - 5
client/src/pages/connect/ConnectContainer.tsx

@@ -9,10 +9,6 @@ const getContainerStyles = makeStyles((theme: Theme) => ({
     backgroundImage: `url(${backgroundPath})`,
     backgroundRepeat: 'no-repeat',
     backgroundSize: 'cover',
-
-    display: 'flex',
-    justifyContent: 'center',
-    alignItems: 'center',
   },
   card: {
     width: '480px',
@@ -28,7 +24,7 @@ const ConnectContainer = ({ children }: { children: ReactElement }) => {
   const classes = getContainerStyles();
 
   return (
-    <main className={classes.wrapper}>
+    <main className={`flex-center ${classes.wrapper}`}>
       <section className={classes.card}>{children}</section>
     </main>
   );

+ 9 - 0
client/src/pages/console/Console.tsx

@@ -0,0 +1,9 @@
+import { useNavigationHook } from '../../hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+
+const Console = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.CONSOLE);
+  return <section>console</section>;
+};
+
+export default Console;

+ 118 - 18
client/src/pages/overview/Overview.tsx

@@ -1,30 +1,130 @@
-import { useContext } from 'react';
+import { makeStyles, Theme, Typography } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
-import { rootContext } from '../../context/Root';
+import EmptyCard from '../../components/cards/EmptyCard';
+import icons from '../../components/icons/Icons';
+import { StatusEnum } from '../../components/status/Types';
+import { useNavigationHook } from '../../hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+import { formatNumber } from '../../utils/Common';
+import CollectionCard from './collectionCard/CollectionCard';
+import { CollectionData } from './collectionCard/Types';
+import StatisticsCard from './statisticsCard/StatisticsCard';
+import { StatisticsCardProps } from './statisticsCard/Types';
 
-const Dashboard = () => {
-  const { setDialog } = useContext(rootContext);
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    margin: theme.spacing(2, 5),
+  },
+  collectionTitle: {
+    margin: theme.spacing(2, 0),
+    lineHeight: '20px',
+    fontSize: '14px',
+    color: '#82838e',
+  },
+  empty: {
+    flexGrow: 1,
+  },
+  emptyIcon: {
+    width: '48px',
+    height: '48px',
+    fill: 'transparent',
 
-  const openDialog = () => {
-    setDialog({
-      open: true,
-      type: 'notice',
-      params: {
-        title: 'test',
-        component: <></>,
-        confirm: () => new Promise((res, rej) => res(true)),
-        cancel: () => new Promise((res, rej) => res(true)),
+    '& path': {
+      stroke: '#aeaebb',
+    },
+  },
+  cardsWrapper: {
+    display: 'grid',
+    gridTemplateColumns: 'repeat(auto-fill, minmax(380px, 1fr))',
+    gap: '10px',
+  },
+}));
+
+const Overview = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
+  const classes = useStyles();
+  const { t } = useTranslation('overview');
+  const { t: collectionTrans } = useTranslation('collection');
+
+  const mockStatistics: StatisticsCardProps = {
+    data: [
+      {
+        label: t('load'),
+        value: formatNumber(4337),
+        valueColor: '#07d197',
+      },
+      {
+        label: t('all'),
+        value: formatNumber(30000),
+        valueColor: '#06aff2',
+      },
+      {
+        label: t('data'),
+        value: t('rows', { number: formatNumber(209379100) }),
+        valueColor: '#0689d2',
       },
-    });
+    ],
   };
 
-  const { t } = useTranslation('btn');
+  const mockCollections: CollectionData[] = [
+    {
+      name: 'collection1',
+      id: 'c1',
+      status: StatusEnum.loaded,
+      rowCount: 2,
+    },
+    {
+      name: 'collection2',
+      id: 'c2',
+      status: StatusEnum.loaded,
+      rowCount: 2,
+    },
+    {
+      name: 'collection3',
+      id: 'c3',
+      status: StatusEnum.loaded,
+      rowCount: 2,
+    },
+    {
+      name: 'collection4',
+      id: 'c4',
+      status: StatusEnum.loaded,
+      rowCount: 2,
+    },
+    {
+      name: 'collection5',
+      id: 'c5',
+      status: StatusEnum.loaded,
+      rowCount: 2,
+    },
+    {
+      name: 'collection6',
+      id: 'c6',
+      status: StatusEnum.loaded,
+      rowCount: 2,
+    },
+  ];
+  const CollectionIcon = icons.navCollection;
 
   return (
-    <section>
-      <button onClick={openDialog}>{t('confirm')}</button>
+    <section className={`page-wrapper ${classes.wrapper}`}>
+      <StatisticsCard data={mockStatistics.data} />
+      <Typography className={classes.collectionTitle}>{t('load')}</Typography>
+      {mockCollections.length > 0 ? (
+        <div className={classes.cardsWrapper}>
+          {mockCollections.map(collection => (
+            <CollectionCard key={collection.id} data={collection} />
+          ))}
+        </div>
+      ) : (
+        <EmptyCard
+          wrapperClass={classes.empty}
+          icon={<CollectionIcon classes={{ root: classes.emptyIcon }} />}
+          text={collectionTrans('noLoadData')}
+        />
+      )}
     </section>
   );
 };
 
-export default Dashboard;
+export default Overview;

+ 97 - 0
client/src/pages/overview/collectionCard/CollectionCard.tsx

@@ -0,0 +1,97 @@
+import {
+  makeStyles,
+  Theme,
+  Link,
+  Typography,
+  Divider,
+} from '@material-ui/core';
+import { FC } from 'react';
+import CustomButton from '../../../components/customButton/CustomButton';
+import icons from '../../../components/icons/Icons';
+import Status from '../../../components/status/Status';
+import CustomToolTip from '../../../components/customToolTip/CustomToolTip';
+import { CollectionCardProps } from './Types';
+import { useTranslation } from 'react-i18next';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    padding: theme.spacing(2),
+    textAlign: 'end',
+  },
+  link: {
+    display: 'flex',
+    alignItems: 'center',
+
+    margin: theme.spacing(2, 0),
+
+    color: '#010e29',
+    fontSize: '20px',
+    lineHeight: '24px',
+    fontWeight: 'bold',
+  },
+  icon: {
+    color: theme.palette.primary.main,
+    marginLeft: theme.spacing(0.5),
+    fontSize: '16px',
+  },
+  content: {
+    display: 'flex',
+    alignItems: 'center',
+    marginBottom: theme.spacing(2),
+  },
+  rowCount: {
+    marginLeft: theme.spacing(3),
+  },
+  divider: {
+    marginBottom: theme.spacing(2),
+  },
+  release: {
+    marginLeft: 0,
+    marginRight: theme.spacing(0.5),
+  },
+}));
+
+const CollectionCard: FC<CollectionCardProps> = ({
+  data,
+  wrapperClass = '',
+}) => {
+  const classes = useStyles();
+  const { name, status, id, rowCount } = data;
+  const RightArrowIcon = icons.rightArrow;
+  const InfoIcon = icons.info;
+  const ReleaseIcon = icons.release;
+  const { t } = useTranslation('collection');
+  const { t: btnTrans } = useTranslation('btn');
+
+  const handleRelease = () => {};
+
+  return (
+    <div className={`card-wrapper ${classes.wrapper} ${wrapperClass}`}>
+      <div>
+        <Status status={status} />
+      </div>
+      <Link
+        classes={{ root: classes.link }}
+        underline="none"
+        href={`/collection/${id}`}
+      >
+        {name}
+        <RightArrowIcon classes={{ root: classes.icon }} />
+      </Link>
+      <div className={classes.content}>
+        <Typography>{t('rowCount')}</Typography>
+        <CustomToolTip title={t('tooltip')} placement="bottom">
+          <InfoIcon classes={{ root: classes.icon }} />
+        </CustomToolTip>
+        <Typography className={classes.rowCount}>{rowCount}</Typography>
+      </div>
+      <Divider classes={{ root: classes.divider }} />
+      <CustomButton variant="contained" onClick={handleRelease}>
+        <ReleaseIcon classes={{ root: `${classes.icon} ${classes.release}` }} />
+        {btnTrans('release')}
+      </CustomButton>
+    </div>
+  );
+};
+
+export default CollectionCard;

+ 13 - 0
client/src/pages/overview/collectionCard/Types.ts

@@ -0,0 +1,13 @@
+import { StatusEnum } from '../../../components/status/Types';
+
+export interface CollectionCardProps {
+  data: CollectionData;
+  wrapperClass?: string;
+}
+
+export interface CollectionData {
+  name: string;
+  status: StatusEnum;
+  id: string;
+  rowCount: number;
+}

+ 52 - 0
client/src/pages/overview/statisticsCard/StatisticsCard.tsx

@@ -0,0 +1,52 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { FC } from 'react';
+import { StatisticsCardProps } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: `grid`,
+    gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
+    columnGap: '20px',
+
+    padding: theme.spacing(3),
+  },
+  itemWrapper: {
+    paddingLeft: theme.spacing(1),
+    borderLeft: '3px solid #f0f4f9',
+  },
+  label: {
+    fontSize: '12px',
+    lineHeight: '16px',
+    color: '#010e29',
+  },
+  value: {
+    fontSize: '24px',
+    lineHeight: '28px',
+    fontWeight: 'bold',
+  },
+}));
+
+const StatisticsCard: FC<StatisticsCardProps> = ({
+  data = [],
+  wrapperClass = '',
+}) => {
+  const classes = useStyles();
+
+  return (
+    <div className={`card-wrapper ${classes.wrapper} ${wrapperClass}`}>
+      {data.map(item => (
+        <div key={item.label} className={classes.itemWrapper}>
+          <Typography className={classes.label}>{item.label}</Typography>
+          <Typography
+            className={classes.value}
+            style={{ color: item.valueColor }}
+          >
+            {item.value}
+          </Typography>
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default StatisticsCard;

+ 10 - 0
client/src/pages/overview/statisticsCard/Types.ts

@@ -0,0 +1,10 @@
+import { KeyValuePair } from '../../../types/Common';
+
+export interface StatisticsCardProps {
+  wrapperClass?: string;
+  data: Item[];
+}
+
+export interface Item extends KeyValuePair {
+  valueColor: string;
+}

+ 12 - 0
client/src/router/Config.ts

@@ -1,4 +1,6 @@
+import Collections from '../pages/collections/Collections';
 import Connect from '../pages/connect/Connect';
+import Console from '../pages/console/Console';
 import Overview from '../pages/overview/Overview';
 
 const RouterConfig = [
@@ -12,6 +14,16 @@ const RouterConfig = [
     component: Connect,
     auth: false,
   },
+  {
+    path: '/collections',
+    component: Collections,
+    auth: true,
+  },
+  {
+    path: '/console',
+    component: Console,
+    auth: true,
+  },
 ];
 
 export default RouterConfig;

+ 21 - 1
client/src/router/Router.tsx

@@ -1,18 +1,38 @@
 import { Switch, Route, BrowserRouter, Redirect } from 'react-router-dom';
 import routerConfig from './Config';
 import Layout from '../components/layout/Layout';
+import { useContext } from 'react';
+import { authContext } from '../context/Auth';
 /**
  * Global responsible for global effect
  * Layout responsible for ui view
  *
  */
 const RouterWrapper = () => {
+  const { isAuth } = useContext(authContext);
+
   return (
     <BrowserRouter>
       <Layout>
         <Switch>
           {routerConfig.map(v => (
-            <Route key={v.path} exact path={v.path} component={v.component} />
+            <Route
+              exact
+              key={v.path}
+              path={v.path}
+              render={() => {
+                const Page = v.component;
+                return isAuth || !v.auth ? (
+                  <Page />
+                ) : (
+                  <Redirect
+                    to={{
+                      pathname: '/connect',
+                    }}
+                  />
+                );
+              }}
+            />
           ))}
 
           <Route

+ 9 - 15
client/src/router/Types.ts

@@ -1,21 +1,15 @@
-export enum ALL_ROUTER_STATES {
-  // '/databases'
-  ALL_DATABASES = 'all_databases',
-  // '/databases/:databaseId'
-  DATABASE_DETAIL = 'database_detail',
-  // '/databases/:databaseId/collections/:collectionId'
+export enum ALL_ROUTER_TYPES {
+  // '/'
+  OVERVIEW = 'overview',
+  // '/collections'
+  COLLECTIONS = 'collections',
+  // '/collections/:collectionId'
   COLLECTION_DETAIL = 'collection_detail',
-  // '/databases/:databaseId/collections/:collectionId/partitions/:partitionId'
-  PARTITION_DETAIL = 'partition_detail',
-  // '/tasks'
-  ALL_TASKS = 'all_tasks',
-  // '/queries'
-  ALL_QUERIES = 'all_queries',
-  // '/queries/:queryId'
-  QUERY_DETAIL = 'query_detail',
+  // '/console'
+  CONSOLE = 'console',
 }
 
 export type NavInfo = {
-  navs: string[];
+  navTitle: string;
   backPath: string;
 };

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

@@ -0,0 +1,19 @@
+/* horizontal and vertical center */
+.flex-center {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.card-wrapper {
+  background-color: #fff;
+  border-radius: 4px;
+  box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.05);
+}
+
+/* used for fill remaining height of page */
+.page-wrapper {
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+}

+ 1 - 2
client/src/utils/Common.ts

@@ -22,8 +22,7 @@ export const generateId = (prefix = 'id') =>
   `${prefix}_${Math.random().toString(36).substr(2, 16)}`;
 
 export const formatNumber = (number: number): string => {
-  const n = number / 100;
-  return (Math.floor(n * 100) / 100).toFixed(2);
+  return new Intl.NumberFormat().format(number);
 };
 
 /**

+ 2 - 2
client/src/utils/Status.ts

@@ -3,8 +3,8 @@ import { StatusEnum } from '../components/status/Types';
 type TaskStatusType = 'success' | 'error' | 'loading';
 
 export const getStatusType = (status: StatusEnum): TaskStatusType => {
-  const loadingList = [StatusEnum.creating];
-  const successList = [StatusEnum.running];
+  const loadingList: StatusEnum[] = [];
+  const successList = [StatusEnum.loaded];
 
   if (loadingList.includes(status)) {
     return 'loading';