Browse Source

Add collection overview page (#430)

* update tree part1

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* add tooltip

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>

* update style

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>

* adjust style

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* add overview page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* add overview page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* rename

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>
ryjiang 1 year ago
parent
commit
d76e5e964a

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

@@ -19,22 +19,9 @@ const useStyles = makeStyles(theme => ({
   root: {
   root: {
     width: '100%',
     width: '100%',
     flexGrow: 1,
     flexGrow: 1,
-    /* set flex basis to make child item height 100% work on Safari */
-    flexBasis: 0,
+    // /* set flex basis to make child item height 100% work on Safari */
+    // flexBasis: 0,
     background: '#fff',
     background: '#fff',
-
-    // change scrollbar style
-    '&::-webkit-scrollbar': {
-      width: '8px',
-    },
-
-    '&::-webkit-scrollbar-track': {
-      backgroundColor: '#f9f9f9',
-    },
-
-    '&::-webkit-scrollbar-thumb': {
-      backgroundColor: '#eee',
-    },
   },
   },
   box: {
   box: {
     backgroundColor: '#fff',
     backgroundColor: '#fff',

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

@@ -710,6 +710,23 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       ></path>
       ></path>
     </SvgIcon>
     </SvgIcon>
   ),
   ),
+  check: (props = {}) => (
+    <SvgIcon
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path
+        d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
+        fill="currentColor"
+        fillRule="evenodd"
+        clipRule="evenodd"
+      ></path>
+    </SvgIcon>
+  ),
 };
 };
 
 
 export default icons;
 export default icons;

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

@@ -46,4 +46,5 @@ export type IconsType =
   | 'prev'
   | 'prev'
   | 'expand'
   | 'expand'
   | 'github'
   | 'github'
-  | 'question';
+  | 'question'
+  | 'check';

+ 3 - 3
client/src/components/status/StatusIcon.tsx

@@ -10,13 +10,13 @@ const useStyles = makeStyles((theme: Theme) => ({
     paddingLeft: theme.spacing(1),
     paddingLeft: theme.spacing(1),
   },
   },
   svg: {
   svg: {
-    color: theme.palette.primary.main,
+    color: theme.palette.primary.light,
   },
   },
 }));
 }));
 
 
 const StatusIcon: FC<StatusIconType> = props => {
 const StatusIcon: FC<StatusIconType> = props => {
   const classes = useStyles();
   const classes = useStyles();
-  const { type, className = '', size = 20 } = props;
+  const { type, className = '', size = 16 } = props;
 
 
   const getElement = (type: ChildrenStatusType): ReactElement => {
   const getElement = (type: ChildrenStatusType): ReactElement => {
     switch (type) {
     switch (type) {
@@ -24,7 +24,7 @@ const StatusIcon: FC<StatusIconType> = props => {
         return (
         return (
           <CircularProgress
           <CircularProgress
             size={size}
             size={size}
-            thickness={8}
+            thickness={4}
             classes={{ svg: classes.svg }}
             classes={{ svg: classes.svg }}
           />
           />
         );
         );

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

@@ -8,9 +8,9 @@ export type StatusType = {
 
 
 export type StatusActionType = {
 export type StatusActionType = {
   status: LOADING_STATE;
   status: LOADING_STATE;
-  percentage?: string;
+  percentage?: string | number;
   action?: Function;
   action?: Function;
-  field: SchemaObject;
+  schema: SchemaObject;
   collectionName: string;
   collectionName: string;
   onIndexCreate?: Function;
   onIndexCreate?: Function;
 };
 };

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

@@ -82,7 +82,7 @@ const collectionTrans = {
 
 
   // collection tabs
   // collection tabs
   partitionTab: '分区',
   partitionTab: '分区',
-  schemaTab: 'Schema',
+  schemaTab: '概览',
   dataTab: '数据',
   dataTab: '数据',
   previewTab: '数据预览',
   previewTab: '数据预览',
   segmentsTab: '数据段(Segments)',
   segmentsTab: '数据段(Segments)',
@@ -127,6 +127,7 @@ const collectionTrans = {
   consistencyEventuallyTooltip:
   consistencyEventuallyTooltip:
     '没有保证读写的顺序,副本最终会在没有进一步写操作的情况下收敛到相同的状态。',
     '没有保证读写的顺序,副本最终会在没有进一步写操作的情况下收敛到相同的状态。',
   releaseCollectionFirst: '请先释放collection.',
   releaseCollectionFirst: '请先释放collection.',
+  noVectorIndexTooltip: '没有向量索引,请先创建一个。',
 
 
   clickToLoad: '点击加载collection。',
   clickToLoad: '点击加载collection。',
   clickToRelease: '点击释放collection。',
   clickToRelease: '点击释放collection。',

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

@@ -17,6 +17,7 @@ const commonTrans = {
     error: '错误',
     error: '错误',
     running: '运行中',
     running: '运行中',
     loading: '正在加载',
     loading: '正在加载',
+    noVectorIndex: '向量索引不存在',
   },
   },
   grid: {
   grid: {
     action: '操作',
     action: '操作',

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

@@ -84,7 +84,7 @@ const collectionTrans = {
 
 
   // collection tabs
   // collection tabs
   partitionTab: 'Partitions',
   partitionTab: 'Partitions',
-  schemaTab: 'Schema',
+  schemaTab: 'Overview',
   dataTab: 'Data',
   dataTab: 'Data',
   previewTab: 'Data Preview',
   previewTab: 'Data Preview',
   segmentsTab: 'Segments',
   segmentsTab: 'Segments',
@@ -125,6 +125,7 @@ const collectionTrans = {
   consistencySessionTooltip: `It ensures that all data writes can be immediately perceived in reads during the same session.`,
   consistencySessionTooltip: `It ensures that all data writes can be immediately perceived in reads during the same session.`,
   consistencyEventuallyTooltip: `There is no guaranteed order of reads and writes, and replicas eventually converge to the same state given that no further write operations are done.`,
   consistencyEventuallyTooltip: `There is no guaranteed order of reads and writes, and replicas eventually converge to the same state given that no further write operations are done.`,
   releaseCollectionFirst: `Please release your collection first.`,
   releaseCollectionFirst: `Please release your collection first.`,
+  noVectorIndexTooltip: `No vector index, create one first.`,
 
 
   clickToLoad: 'Click to load the collection.',
   clickToLoad: 'Click to load the collection.',
   clickToRelease: 'Click to release the collection.',
   clickToRelease: 'Click to release the collection.',

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

@@ -1,5 +1,3 @@
-import { partition } from "d3";
-
 const commonTrans = {
 const commonTrans = {
   attu: {
   attu: {
     admin: 'Attu',
     admin: 'Attu',
@@ -19,6 +17,7 @@ const commonTrans = {
     error: 'error',
     error: 'error',
     running: 'running',
     running: 'running',
     loading: 'is loaded',
     loading: 'is loaded',
+    noVectorIndex: 'No Vector Index',
   },
   },
   grid: {
   grid: {
     action: 'action',
     action: 'action',

+ 3 - 60
client/src/pages/collections/Collections.tsx

@@ -1,6 +1,6 @@
 import { useCallback, useContext, useMemo, useState } from 'react';
 import { useCallback, useContext, useMemo, useState } from 'react';
 import { Link, useSearchParams } from 'react-router-dom';
 import { Link, useSearchParams } from 'react-router-dom';
-import { makeStyles, Theme, Chip, Tooltip } from '@material-ui/core';
+import { makeStyles, Theme } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import Highlighter from 'react-highlight-words';
 import Highlighter from 'react-highlight-words';
 import { rootContext, authContext, dataContext } from '@/context';
 import { rootContext, authContext, dataContext } from '@/context';
@@ -93,13 +93,6 @@ const Collections = () => {
   const QuestionIcon = icons.question;
   const QuestionIcon = icons.question;
   const SourceIcon = icons.source;
   const SourceIcon = icons.source;
 
 
-  const consistencyTooltipsMap: Record<string, string> = {
-    Strong: collectionTrans('consistencyStrongTooltip'),
-    Bounded: collectionTrans('consistencyBoundedTooltip'),
-    Session: collectionTrans('consistencySessionTooltip'),
-    Eventually: collectionTrans('consistencyEventuallyTooltip'),
-  };
-
   const clearIndexCache = useCallback(async () => {
   const clearIndexCache = useCallback(async () => {
     await CollectionService.flush();
     await CollectionService.flush();
   }, []);
   }, []);
@@ -348,7 +341,7 @@ const Collections = () => {
       formatter({ collection_name }) {
       formatter({ collection_name }) {
         return (
         return (
           <Link
           <Link
-            to={`/databases/${database}/${collection_name}/data`}
+            to={`/databases/${database}/${collection_name}/info`}
             className={classes.link}
             className={classes.link}
             title={collection_name}
             title={collection_name}
           >
           >
@@ -373,7 +366,7 @@ const Collections = () => {
           <StatusAction
           <StatusAction
             status={v.status}
             status={v.status}
             percentage={v.loadedPercentage}
             percentage={v.loadedPercentage}
-            field={v.schema}
+            schema={v.schema}
             collectionName={v.collection_name}
             collectionName={v.collection_name}
             action={() => {
             action={() => {
               setDialog({
               setDialog({
@@ -397,56 +390,6 @@ const Collections = () => {
         );
         );
       },
       },
     },
     },
-    {
-      id: 'collection_name',
-      align: 'left',
-      disablePadding: true,
-      notSort: true,
-      label: collectionTrans('features'),
-      formatter(v) {
-        return (
-          <>
-            {v.autoID ? (
-              <Tooltip
-                title={collectionTrans('autoIDTooltip')}
-                placement="top"
-                arrow
-              >
-                <Chip
-                  className={classes.chip}
-                  label={collectionTrans('autoID')}
-                  size="small"
-                />
-              </Tooltip>
-            ) : null}
-            {v.schema && v.schema.enable_dynamic_field ? (
-              <Tooltip
-                title={collectionTrans('dynamicSchemaTooltip')}
-                placement="top"
-                arrow
-              >
-                <Chip
-                  className={classes.chip}
-                  label={collectionTrans('dynamicSchema')}
-                  size="small"
-                />
-              </Tooltip>
-            ) : null}
-            <Tooltip
-              title={consistencyTooltipsMap[v.consistency_level] || ''}
-              placement="top"
-              arrow
-            >
-              <Chip
-                className={classes.chip}
-                label={v.consistency_level}
-                size="small"
-              />
-            </Tooltip>
-          </>
-        );
-      },
-    },
     {
     {
       id: 'rowCount',
       id: 'rowCount',
       align: 'left',
       align: 'left',

+ 43 - 39
client/src/pages/collections/StatusAction.tsx

@@ -10,13 +10,12 @@ import {
   createStyles,
   createStyles,
   Typography,
   Typography,
   useTheme,
   useTheme,
-  Tooltip,
   Chip,
   Chip,
 } from '@material-ui/core';
 } from '@material-ui/core';
 import { LOADING_STATE } from '@/consts';
 import { LOADING_STATE } from '@/consts';
 import StatusIcon from '@/components/status/StatusIcon';
 import StatusIcon from '@/components/status/StatusIcon';
 import icons from '@/components/icons/Icons';
 import icons from '@/components/icons/Icons';
-import IndexTypeElement from '@/pages/schema/IndexTypeElement';
+import CustomToolTip from '@/components/customToolTip/CustomToolTip';
 
 
 const useStyles = makeStyles((theme: Theme) =>
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
   createStyles({
@@ -31,17 +30,22 @@ const useStyles = makeStyles((theme: Theme) =>
       paddingLeft: theme.spacing(0.5),
       paddingLeft: theme.spacing(0.5),
     },
     },
     circle: {
     circle: {
-      backgroundColor: (props: any) => props.color,
+      width: '8px',
+      height: '8px',
       borderRadius: '50%',
       borderRadius: '50%',
-      width: '10px',
-      height: '10px',
+      backgroundColor: theme.palette.primary.main,
     },
     },
-
-    circleUnload: {
-      backgroundColor: theme.palette.attuGrey.main,
-      borderRadius: '50%',
-      width: '10px',
-      height: '10px',
+    loaded: {
+      border: `1px solid ${theme.palette.primary.main}`,
+      backgroundColor: theme.palette.primary.main,
+    },
+    unloaded: {
+      border: `1px solid ${theme.palette.primary.main}`,
+      background: '#fff !important',
+    },
+    noIndex: {
+      border: `1px solid ${theme.palette.attuGrey.light}`,
+      backgroundColor: '#fff',
     },
     },
 
 
     loading: {
     loading: {
@@ -78,7 +82,7 @@ const StatusAction: FC<StatusActionType> = props => {
     status,
     status,
     percentage = 0,
     percentage = 0,
     collectionName,
     collectionName,
-    field,
+    schema,
     action = () => {},
     action = () => {},
   } = props;
   } = props;
   const { t: commonTrans } = useTranslation();
   const { t: commonTrans } = useTranslation();
@@ -95,7 +99,7 @@ const StatusAction: FC<StatusActionType> = props => {
       case LOADING_STATE.UNLOADED:
       case LOADING_STATE.UNLOADED:
         return {
         return {
           label: statusTrans.unloaded,
           label: statusTrans.unloaded,
-          icon: <div className={`${classes.circleUnload}`}></div>,
+          icon: <div className={`${classes.circle} ${classes.unloaded}`}></div>,
           tooltip: collectionTrans('clickToLoad'),
           tooltip: collectionTrans('clickToLoad'),
           deleteIcon: <LoadIcon />,
           deleteIcon: <LoadIcon />,
         };
         };
@@ -103,7 +107,7 @@ const StatusAction: FC<StatusActionType> = props => {
       case LOADING_STATE.LOADED:
       case LOADING_STATE.LOADED:
         return {
         return {
           label: statusTrans.loaded,
           label: statusTrans.loaded,
-          icon: <div className={classes.circle}></div>,
+          icon: <div className={`${classes.circle} ${classes.loaded}`}></div>,
           tooltip: collectionTrans('clickToRelease'),
           tooltip: collectionTrans('clickToRelease'),
           deleteIcon: <ReleaseIcon />,
           deleteIcon: <ReleaseIcon />,
         };
         };
@@ -129,33 +133,33 @@ const StatusAction: FC<StatusActionType> = props => {
     }
     }
   }, [status, statusTrans, percentage]);
   }, [status, statusTrans, percentage]);
 
 
-  // UI state
-  const collectionLoaded = status === LOADING_STATE.LOADED;
+  const noIndex = !schema.hasVectorIndex;
+  const noVectorIndexTooltip = collectionTrans('noVectorIndexTooltip');
+  const noIndexIcon = (
+    <div className={`${classes.circle} ${classes.noIndex}`}></div>
+  );
+
   return (
   return (
     <div className={classes.root}>
     <div className={classes.root}>
-      {field.hasVectorIndex && (
-        <Tooltip arrow interactive title={tooltip} placement={'top'}>
-          <Chip
-            className={classes.chip}
-            variant="outlined"
-            label={<Typography>{label}</Typography>}
-            onDelete={() => action()}
-            onClick={(e: MouseEvent<HTMLDivElement>) => {
-              e.stopPropagation();
-              action();
-            }}
-            deleteIcon={deleteIcon}
-            size="small"
-            icon={icon}
-          />
-        </Tooltip>
-      )}
-      <IndexTypeElement
-        field={field.vectorFields[0]!}
-        collectionName={collectionName}
-        disabled={collectionLoaded}
-        disabledTooltip={collectionTrans('releaseCollectionFirst')}
-      />
+      <CustomToolTip
+        title={noIndex ? noVectorIndexTooltip : tooltip}
+        placement={'top'}
+      >
+        <Chip
+          className={classes.chip}
+          variant="outlined"
+          label={<Typography>{label}</Typography>}
+          onDelete={() => action()}
+          onClick={(e: MouseEvent<HTMLDivElement>) => {
+            e.stopPropagation();
+            action();
+          }}
+          disabled={noIndex}
+          deleteIcon={deleteIcon}
+          size="small"
+          icon={noIndex ? noIndexIcon : icon}
+        />
+      </CustomToolTip>
     </div>
     </div>
   );
   );
 };
 };

+ 8 - 6
client/src/pages/databases/Databases.tsx

@@ -17,7 +17,6 @@ import Collections from '../collections/Collections';
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
     flexDirection: 'row',
     flexDirection: 'row',
-    gap: theme.spacing(2),
   },
   },
   tree: {
   tree: {
     boxShadow: 'none',
     boxShadow: 'none',
@@ -25,13 +24,16 @@ const useStyles = makeStyles((theme: Theme) => ({
     width: theme.spacing(28),
     width: theme.spacing(28),
     flexGrow: 0,
     flexGrow: 0,
     flexShrink: 0,
     flexShrink: 0,
+    height: 'calc(100vh - 96px)',
     overflow: 'auto',
     overflow: 'auto',
     boxSizing: 'border-box',
     boxSizing: 'border-box',
+    padding: theme.spacing(0, 2, 0, 0),
   },
   },
   tab: {
   tab: {
     flexGrow: 1,
     flexGrow: 1,
     flexShrink: 1,
     flexShrink: 1,
     overflowX: 'auto',
     overflowX: 'auto',
+    padding: theme.spacing(0, 2),
   },
   },
 }));
 }));
 
 
@@ -65,16 +67,16 @@ const Databases = () => {
 
 
   // collection tabs
   // collection tabs
   const collectionTabs: ITab[] = [
   const collectionTabs: ITab[] = [
+    {
+      label: collectionTrans('schemaTab'),
+      component: <Schema />,
+      path: `info`,
+    },
     {
     {
       label: collectionTrans('dataTab'),
       label: collectionTrans('dataTab'),
       component: <Query />,
       component: <Query />,
       path: `data`,
       path: `data`,
     },
     },
-    {
-      label: collectionTrans('schemaTab'),
-      component: <Schema />,
-      path: `schema`,
-    },
     {
     {
       label: collectionTrans('partitionTab'),
       label: collectionTrans('partitionTab'),
       component: <Partitions />,
       component: <Partitions />,

+ 89 - 3
client/src/pages/databases/tree/index.tsx

@@ -1,9 +1,11 @@
+import { useTranslation } from 'react-i18next';
 import TreeView from '@material-ui/lab/TreeView';
 import TreeView from '@material-ui/lab/TreeView';
 import TreeItem from '@material-ui/lab/TreeItem';
 import TreeItem from '@material-ui/lab/TreeItem';
 import icons from '@/components/icons/Icons';
 import icons from '@/components/icons/Icons';
-import { makeStyles, Theme } from '@material-ui/core';
+import { makeStyles, Theme, Tooltip } from '@material-ui/core';
 import { useNavigate, Params } from 'react-router-dom';
 import { useNavigate, Params } from 'react-router-dom';
 import { CollectionObject } from '@server/types';
 import { CollectionObject } from '@server/types';
+import clcx from 'clsx';
 
 
 export type TreeNodeType = 'db' | 'collection' | 'partition' | 'segment';
 export type TreeNodeType = 'db' | 'collection' | 'partition' | 'segment';
 
 
@@ -13,6 +15,7 @@ export interface DatabaseTreeItem {
   name: string;
   name: string;
   type: TreeNodeType;
   type: TreeNodeType;
   expanded?: boolean;
   expanded?: boolean;
+  data?: CollectionObject;
 }
 }
 
 
 interface DatabaseToolProps {
 interface DatabaseToolProps {
@@ -37,6 +40,7 @@ const getExpanded = (nodes: DatabaseTreeItem[]) => {
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   root: {
   root: {
+    fontSize: '15px',
     '& .MuiTreeItem-iconContainer': {
     '& .MuiTreeItem-iconContainer': {
       width: 'auto',
       width: 'auto',
     },
     },
@@ -50,6 +54,8 @@ const useStyles = makeStyles((theme: Theme) => ({
       backgroundColor: 'none',
       backgroundColor: 'none',
     },
     },
     '& .MuiTreeItem-content': {
     '& .MuiTreeItem-content': {
+      width: 'auto',
+
       '&:hover': {
       '&:hover': {
         backgroundColor: 'rgba(10, 206, 130, 0.08)',
         backgroundColor: 'rgba(10, 206, 130, 0.08)',
       },
       },
@@ -79,8 +85,87 @@ const useStyles = makeStyles((theme: Theme) => ({
       color: '#888',
       color: '#888',
     },
     },
   },
   },
+  collectionNode: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    minHeight: '24px',
+    lineHeight: '24px',
+  },
+  right: {
+    display: 'flex',
+    alignItems: 'center',
+    width: 20,
+  },
+  count: {
+    fontSize: '13px',
+    fontWeight: 500,
+    marginLeft: theme.spacing(0.5),
+    color: theme.palette.attuGrey.main,
+  },
+  dot: {
+    width: '8px',
+    height: '8px',
+    borderRadius: '50%',
+    position: 'relative',
+    top: '0',
+  },
+  loaded: {
+    border: `1px solid ${theme.palette.primary.main}`,
+    backgroundColor: theme.palette.primary.main,
+  },
+  unloaded: {
+    border: `1px solid ${theme.palette.primary.main}`,
+    background: '#fff !important',
+  },
+  loading: {
+    border: `1px solid ${theme.palette.primary.light}`,
+    backgroundColor: `${theme.palette.primary.light} !important`,
+  },
+  noIndex: {
+    border: `1px solid ${theme.palette.attuGrey.light}`,
+    backgroundColor: theme.palette.attuGrey.light,
+  },
 }));
 }));
 
 
+const CollectionNode: React.FC<{ data: CollectionObject }> = ({ data }) => {
+  // i18n collectionTrans
+  const { t: commonTrans } = useTranslation();
+  const statusTrans = commonTrans('status');
+
+  // styles
+  const classes = useStyles();
+
+  // class
+  const loadClass = clcx(classes.dot, {
+    [classes.loaded]: data.loaded,
+    [classes.unloaded]: !data.loaded,
+    [classes.loading]: data.status === 'loading',
+    [classes.noIndex]: !data.schema || !data.schema.hasVectorIndex,
+  });
+
+  //  status tooltip
+  const hasIndex = data.schema && data.schema.hasVectorIndex;
+  const loadStatus = hasIndex
+    ? data.loaded
+      ? statusTrans.loaded
+      : statusTrans.unloaded
+    : statusTrans.noVectorIndex;
+
+  return (
+    <div className={classes.collectionNode}>
+      <div>
+        {data.collection_name}
+        <span className={classes.count}>({data.rowCount})</span>
+      </div>
+      <div className={classes.right}>
+        <Tooltip title={loadStatus}>
+          <div className={loadClass}></div>
+        </Tooltip>
+      </div>
+    </div>
+  );
+};
+
 const DatabaseTree: React.FC<DatabaseToolProps> = props => {
 const DatabaseTree: React.FC<DatabaseToolProps> = props => {
   // props
   // props
   const { database, collections, params } = props;
   const { database, collections, params } = props;
@@ -91,6 +176,7 @@ const DatabaseTree: React.FC<DatabaseToolProps> = props => {
       id: `c_${c.collection_name}`,
       id: `c_${c.collection_name}`,
       name: c.collection_name,
       name: c.collection_name,
       type: 'collection' as TreeNodeType,
       type: 'collection' as TreeNodeType,
+      data: c,
     };
     };
   });
   });
 
 
@@ -117,7 +203,7 @@ const DatabaseTree: React.FC<DatabaseToolProps> = props => {
       node.type === 'db'
       node.type === 'db'
         ? `/databases/${database}/${params.databasePage || 'collections'}`
         ? `/databases/${database}/${params.databasePage || 'collections'}`
         : `/databases/${database}/${node.name}/${
         : `/databases/${database}/${node.name}/${
-            params.collectionPage || 'data'
+            params.collectionPage || 'info'
           }`
           }`
     );
     );
   };
   };
@@ -150,7 +236,7 @@ const DatabaseTree: React.FC<DatabaseToolProps> = props => {
           key={node.id}
           key={node.id}
           nodeId={node.id}
           nodeId={node.id}
           icon={<CollectionIcon />}
           icon={<CollectionIcon />}
-          label={node.name}
+          label={<CollectionNode data={node.data!} />}
           className={classes.treeItem}
           className={classes.treeItem}
           onClick={event => {
           onClick={event => {
             event.stopPropagation();
             event.stopPropagation();

+ 1 - 1
client/src/pages/dialogs/LoadCollectionDialog.tsx

@@ -181,7 +181,7 @@ const LoadCollectionDialog = (props: any) => {
           <Typography variant="body1" component="p" className={classes.desc}>
           <Typography variant="body1" component="p" className={classes.desc}>
             {collectionTrans('loadContent')}
             {collectionTrans('loadContent')}
           </Typography>
           </Typography>
-          {!enableRelica ? (
+          {enableRelica ? (
             <>
             <>
               <FormControlLabel
               <FormControlLabel
                 control={
                 control={

+ 166 - 43
client/src/pages/schema/Schema.tsx

@@ -1,25 +1,55 @@
-import { makeStyles, Theme, Typography, Chip } from '@material-ui/core';
+import {
+  makeStyles,
+  Theme,
+  Typography,
+  Chip,
+  Tooltip,
+} from '@material-ui/core';
 import { useContext } from 'react';
 import { useContext } from 'react';
 import { useParams } from 'react-router-dom';
 import { useParams } from 'react-router-dom';
 import AttuGrid from '@/components/grid/Grid';
 import AttuGrid from '@/components/grid/Grid';
 import { ColDefinitionsType } from '@/components/grid/Types';
 import { ColDefinitionsType } from '@/components/grid/Types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { usePaginationHook } from '@/hooks';
 import icons from '@/components/icons/Icons';
 import icons from '@/components/icons/Icons';
 import { formatFieldType } from '@/utils';
 import { formatFieldType } from '@/utils';
-import { dataContext } from '@/context';
+import { rootContext, dataContext } from '@/context';
 import IndexTypeElement from './IndexTypeElement';
 import IndexTypeElement from './IndexTypeElement';
 import { getLabelDisplayedRows } from '../search/Utils';
 import { getLabelDisplayedRows } from '../search/Utils';
+import { LOADING_STATE } from '@/consts';
+import LoadCollectionDialog from '@/pages/dialogs/LoadCollectionDialog';
+import ReleaseCollectionDialog from '@/pages/dialogs/ReleaseCollectionDialog';
+import StatusAction from '@/pages/collections/StatusAction';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
-    height: `calc(100vh - 160px)`,
+    display: 'flex',
+    flexDirection: 'column',
+    flexGrow: 1,
+    height: `100%`,
+    overflow: 'auto',
+    '& h5': {
+      color: theme.palette.attuGrey.dark,
+      marginBottom: theme.spacing(0.5),
+      fontSize: '14px',
+      fontWeight: 400,
+    },
+  },
+  infoWrapper: {
+    marginBottom: theme.spacing(2),
+    paddingTop: theme.spacing(0.5),
+  },
+  block: {
+    '& *': {
+      fontSize: '14px',
+      lineHeight: 1.5,
+    },
+    paddingBottom: theme.spacing(2),
   },
   },
   icon: {
   icon: {
     fontSize: '20px',
     fontSize: '20px',
     marginLeft: theme.spacing(0.5),
     marginLeft: theme.spacing(0.5),
   },
   },
-  iconTitle: {
+  primaryKeyChip: {
     fontSize: '8px',
     fontSize: '8px',
     position: 'relative',
     position: 'relative',
     top: '3px',
     top: '3px',
@@ -28,6 +58,13 @@ const useStyles = makeStyles((theme: Theme) => ({
   chip: {
   chip: {
     marginLeft: theme.spacing(0.5),
     marginLeft: theme.spacing(0.5),
     marginRight: theme.spacing(0.5),
     marginRight: theme.spacing(0.5),
+    fontSize: '12px',
+    background: 'rgba(0, 0, 0, 0.04)',
+    border: 'none',
+  },
+  featureChip: {
+    marginLeft: 0,
+    border: 'none',
   },
   },
   nameWrapper: {
   nameWrapper: {
     display: 'flex',
     display: 'flex',
@@ -58,9 +95,14 @@ const useStyles = makeStyles((theme: Theme) => ({
       },
       },
     },
     },
   },
   },
+
+  gridWrapper: {
+    paddingBottom: theme.spacing(2),
+  },
 }));
 }));
 
 
 const Schema = () => {
 const Schema = () => {
+  const { setDialog } = useContext(rootContext);
   const { fetchCollection, collections, loading } = useContext(dataContext);
   const { fetchCollection, collections, loading } = useContext(dataContext);
   const { collectionName = '' } = useParams<{ collectionName: string }>();
   const { collectionName = '' } = useParams<{ collectionName: string }>();
   const classes = useStyles();
   const classes = useStyles();
@@ -69,27 +111,23 @@ const Schema = () => {
   const { t: commonTrans } = useTranslation();
   const { t: commonTrans } = useTranslation();
   const gridTrans = commonTrans('grid');
   const gridTrans = commonTrans('grid');
 
 
+  const consistencyTooltipsMap: Record<string, string> = {
+    Strong: collectionTrans('consistencyStrongTooltip'),
+    Bounded: collectionTrans('consistencyBoundedTooltip'),
+    Session: collectionTrans('consistencySessionTooltip'),
+    Eventually: collectionTrans('consistencyEventuallyTooltip'),
+  };
+
   // get collection
   // get collection
   const collection = collections.find(
   const collection = collections.find(
     c => c.collection_name === collectionName
     c => c.collection_name === collectionName
   );
   );
 
 
   // get fields
   // get fields
-  const fileds = collection?.schema?.fields || [];
+  const fields = collection?.schema?.fields || [];
 
 
   const KeyIcon = icons.key;
   const KeyIcon = icons.key;
-
-  const {
-    pageSize,
-    handlePageSize,
-    currentPage,
-    handleCurrentPage,
-    total,
-    data: schemaList,
-    order,
-    orderBy,
-    handleGridSort,
-  } = usePaginationHook(fileds);
+  const EnabledIcon = icons.check;
 
 
   const colDefinitions: ColDefinitionsType[] = [
   const colDefinitions: ColDefinitionsType[] = [
     {
     {
@@ -102,7 +140,7 @@ const Schema = () => {
             {f.name}
             {f.name}
             {f.is_primary_key ? (
             {f.is_primary_key ? (
               <div
               <div
-                className={classes.iconTitle}
+                className={classes.primaryKeyChip}
                 title={collectionTrans('idFieldName')}
                 title={collectionTrans('idFieldName')}
               >
               >
                 <KeyIcon classes={{ root: 'key' }} />
                 <KeyIcon classes={{ root: 'key' }} />
@@ -209,32 +247,117 @@ const Schema = () => {
     },
     },
   ];
   ];
 
 
-  const handlePageChange = (e: any, page: number) => {
-    handleCurrentPage(page);
-  };
-
+  // get loading state label
   return (
   return (
     <section className={classes.wrapper}>
     <section className={classes.wrapper}>
-      <AttuGrid
-        toolbarConfigs={[]}
-        colDefinitions={colDefinitions}
-        rows={schemaList}
-        rowCount={total}
-        primaryKey="fieldID"
-        showHoverStyle={false}
-        page={currentPage}
-        onPageChange={handlePageChange}
-        rowsPerPage={pageSize}
-        setRowsPerPage={handlePageSize}
-        isLoading={loading}
-        openCheckBox={false}
-        order={order}
-        orderBy={orderBy}
-        handleSort={handleGridSort}
-        labelDisplayedRows={getLabelDisplayedRows(
-          gridTrans[schemaList.length > 1 ? 'fields' : 'field']
-        )}
-      />
+      {collection && (
+        <section className={classes.infoWrapper}>
+          <div className={classes.block}>
+            <Typography variant="h5">{collectionTrans('status')}</Typography>
+            <StatusAction
+              status={collection.status}
+              percentage={collection.loadedPercentage}
+              schema={collection.schema!}
+              collectionName={collection.collection_name}
+              action={() => {
+                setDialog({
+                  open: true,
+                  type: 'custom',
+                  params: {
+                    component:
+                      collection.status === LOADING_STATE.UNLOADED ? (
+                        <LoadCollectionDialog
+                          collectionName={collection.collection_name}
+                        />
+                      ) : (
+                        <ReleaseCollectionDialog
+                          collectionName={collection.collection_name}
+                        />
+                      ),
+                  },
+                });
+              }}
+            />
+          </div>
+
+          <div className={classes.block}>
+            <Typography variant="h5">
+              {collectionTrans('description')}
+            </Typography>
+            <Typography variant="h6">
+              {collection?.description || '--'}
+            </Typography>
+          </div>
+
+          <div className={classes.block}>
+            <Typography variant="h5">
+              {collectionTrans('consistency')}
+            </Typography>
+            <Typography variant="h6">
+              <Tooltip
+                title={
+                  consistencyTooltipsMap[collection.consistency_level!] || ''
+                }
+                placement="top"
+                arrow
+              >
+                <Chip
+                  className={`${classes.chip} ${classes.featureChip}`}
+                  label={collection.consistency_level}
+                  variant="outlined"
+                  size="small"
+                />
+              </Tooltip>
+            </Typography>
+          </div>
+
+          <div className={classes.block}>
+            <Typography variant="h5">
+              {collectionTrans('createdTime')}
+            </Typography>
+            <Typography variant="h6">
+              {new Date(collection.createdTime).toLocaleString()}
+            </Typography>
+          </div>
+        </section>
+      )}
+
+      <section className={classes.gridWrapper}>
+        <Typography variant="h5">
+          {collectionTrans('schema')}
+          {collection &&
+          collection.schema &&
+          collection.schema.enable_dynamic_field ? (
+            <Tooltip
+              title={collectionTrans('dynamicSchemaTooltip')}
+              placement="top"
+              arrow
+            >
+              <Chip
+                className={`${classes.chip}`}
+                label={collectionTrans('dynamicSchema')}
+                size="small"
+                icon={<EnabledIcon />}
+              />
+            </Tooltip>
+          ) : null}
+        </Typography>
+
+        <AttuGrid
+          toolbarConfigs={[]}
+          colDefinitions={colDefinitions}
+          rows={fields}
+          rowCount={fields.length}
+          primaryKey="fieldID"
+          showHoverStyle={false}
+          isLoading={loading}
+          openCheckBox={false}
+          showPagination={false}
+          labelDisplayedRows={getLabelDisplayedRows(
+            gridTrans[fields.length > 1 ? 'fields' : 'field']
+          )}
+        />
+      </section>
     </section>
     </section>
   );
   );
 };
 };

+ 1 - 1
client/src/styles/theme.ts

@@ -42,7 +42,7 @@ const commonThemes = {
   palette: {
   palette: {
     primary: {
     primary: {
       main: '#0ACE82',
       main: '#0ACE82',
-      light: '#bfdeff',
+      light: '#65BA74',
       dark: '#08a568',
       dark: '#08a568',
     },
     },
     secondary: {
     secondary: {

+ 1 - 1
server/src/collections/collections.service.ts

@@ -438,7 +438,7 @@ export class CollectionsService {
         v => v.name === collection.name
         v => v.name === collection.name
       );
       );
 
 
-      const notLazy = !!loadedCollection || i < 5; // lazy is true, only load full details for the first 10 collections
+      const notLazy = i <= 5; // lazy is true, only load full details for the first 10 collections
 
 
       data.push(
       data.push(
         await this.getCollection(
         await this.getCollection(