2
0
Эх сурвалжийг харах

Add a simple collection tree (#416)

* clean ui console errors

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

* Add a simple tree as collections selector

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

---------

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>
Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 1 жил өмнө
parent
commit
f24f5a44d1

+ 117 - 0
client/src/components/CustomTree/index.tsx

@@ -0,0 +1,117 @@
+import TreeView from '@material-ui/lab/TreeView';
+import TreeItem from '@material-ui/lab/TreeItem';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+
+export type TreeNodeType = 'db' | 'collection' | 'partition' | 'segment';
+
+export interface CustomTreeItem {
+  children?: CustomTreeItem[];
+  id: string;
+  name: string;
+  type: TreeNodeType;
+  expanded?: boolean;
+}
+
+interface CustomToolProps {
+  data: CustomTreeItem;
+  onNodeToggle?: (event: React.ChangeEvent<{}>, nodeIds: string[]) => void;
+  multiSelect?: true;
+  disableSelection?: boolean;
+  defaultSelected?: string[];
+  onNodeClick?: (node: CustomTreeItem) => void;
+}
+
+// get expanded nodes from data
+const getExpanded = (nodes: CustomTreeItem[]) => {
+  const expanded: string[] = [];
+  nodes.forEach(node => {
+    if (node.expanded) {
+      expanded.push(node.id);
+    }
+    if (node.children && node.children.length > 0) {
+      expanded.push(...getExpanded(node.children));
+    }
+  });
+  return expanded;
+};
+
+const CustomTree: React.FC<CustomToolProps> = ({
+  data,
+  onNodeToggle,
+  multiSelect,
+  disableSelection,
+  defaultSelected = [],
+  onNodeClick,
+}) => {
+  // UI data
+  const expanded = getExpanded([data]);
+  // render children
+  const renderTree = (nodes: CustomTreeItem[]) => {
+    return nodes.map(node => {
+      if (node.children && node.children.length > 0) {
+        return (
+          <TreeItem
+            key={node.id}
+            nodeId={node.id}
+            label={node.name}
+            onClick={event => {
+              event.stopPropagation();
+              if (onNodeClick) {
+                onNodeClick(node);
+              }
+            }}
+          >
+            {renderTree(node.children)}
+          </TreeItem>
+        );
+      }
+
+      return (
+        <TreeItem
+          key={node.id}
+          nodeId={node.id}
+          label={node.name}
+          onClick={event => {
+            event.stopPropagation();
+            if (onNodeClick) {
+              onNodeClick(node);
+            }
+          }}
+        />
+      );
+    });
+  };
+
+  return (
+    <TreeView
+      defaultCollapseIcon={<ExpandMoreIcon />}
+      defaultExpandIcon={<ChevronRightIcon />}
+      expanded={expanded}
+      onNodeToggle={onNodeToggle}
+      selected={defaultSelected}
+      multiSelect={multiSelect}
+      disableSelection={disableSelection}
+    >
+      {data && (
+        <TreeItem
+          key={data.id}
+          nodeId={data.id}
+          label={data.name}
+          onClick={event => {
+            event.stopPropagation();
+            if (onNodeClick) {
+              onNodeClick(data);
+            }
+          }}
+        >
+          {data.children && data.children.length > 0
+            ? renderTree(data.children)
+            : [<div key="stub" />]}
+        </TreeItem>
+      )}
+    </TreeView>
+  );
+};
+
+export default CustomTree;

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

@@ -29,6 +29,7 @@ import DatePicker from '@material-ui/icons/Event';
 import GetAppIcon from '@material-ui/icons/GetApp';
 // import PersonOutlineIcon from '@material-ui/icons/PersonOutline';
 import PersonOutlineIcon from '@material-ui/icons/Person';
+import SettingsIcon from '@material-ui/icons/Settings';
 import { SvgIcon } from '@material-ui/core';
 import ZillizIcon from '@/assets/icons/attu.svg?react';
 import OverviewIcon from '@/assets/icons/overview.svg?react';
@@ -44,6 +45,7 @@ import SearchEmptyIcon from '@/assets/icons/search.svg?react';
 import CopyIcon from '@/assets/icons/copy.svg?react';
 import SystemIcon from '@/assets/icons/system.svg?react';
 import Compact from '@/assets/icons/compact.svg?react';
+import Settings from '@material-ui/icons/Settings';
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
@@ -100,6 +102,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   navSystem: (props = {}) => (
     <SvgIcon viewBox="0 0 20 20" component={SystemIcon} {...props} />
   ),
+  settings: (props = {}) => (
+    <SvgIcon viewBox="0 0 24 24" component={SettingsIcon} {...props} />
+  ),
   info: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
   ),
@@ -118,7 +123,12 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
     </svg>
   ),
   upload: (props = {}) => (
-    <SvgIcon viewBox="0 0 20 20" component={UploadIcon} {...props} fill="currentColor" />
+    <SvgIcon
+      viewBox="0 0 20 20"
+      component={UploadIcon}
+      {...props}
+      fill="currentColor"
+    />
   ),
   vectorSearch: (props = {}) => (
     <SvgIcon viewBox="0 0 48 48" component={SearchEmptyIcon} {...props} />

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

@@ -44,4 +44,5 @@ export type IconsType =
   | 'database'
   | 'uploadFile'
   | 'compact'
-  | 'saveAs';
+  | 'saveAs'
+  | 'settings';

+ 8 - 3
client/src/context/Data.tsx

@@ -9,7 +9,7 @@ import {
 import { io, Socket } from 'socket.io-client';
 import { authContext } from '@/context';
 import { url, CollectionService, MilvusService, DatabaseService } from '@/http';
-import { checkIndexBuilding, checkLoading } from '@/utils';
+import { checkIndexBuilding, checkLoading, getDbValueFromUrl } from '@/utils';
 import { DataContextType } from './Types';
 import { WS_EVENTS, WS_EVENTS_TYPE } from '@server/utils/Const';
 import { LAST_TIME_DATABASE } from '@/consts';
@@ -30,14 +30,19 @@ export const dataContext = createContext<DataContextType>({
 const { Provider } = dataContext;
 
 export const DataProvider = (props: { children: React.ReactNode }) => {
+  // get database name from url
+  const currentUrl = window.location.href;
+  const initialDatabase = getDbValueFromUrl(currentUrl);
   // local data state
   const [collections, setCollections] = useState<CollectionObject[]>([]);
   const [connected, setConnected] = useState(false);
   const [loading, setLoading] = useState(false);
   const [database, setDatabase] = useState<string>(
-    window.localStorage.getItem(LAST_TIME_DATABASE) || 'default'
+    initialDatabase ||
+      window.localStorage.getItem(LAST_TIME_DATABASE) ||
+      'default'
   );
-  const [databases, setDatabases] = useState<string[]>(['default']);
+  const [databases, setDatabases] = useState<string[]>([database]);
   // auth context
   const { isAuth, clientId } = useContext(authContext);
   // socket ref

+ 5 - 5
client/src/hooks/Navigation.ts

@@ -29,16 +29,16 @@ export const useNavigationHook = (
       }
       case ALL_ROUTER_TYPES.DB_ADMIN: {
         const navInfo: NavInfo = {
-          navTitle: navTrans('database'),
+          navTitle: navTrans('dbAdmin'),
           backPath: '',
           showDatabaseSelector: false,
         };
         setNavInfo(navInfo);
         break;
       }
-      case ALL_ROUTER_TYPES.COLLECTIONS: {
+      case ALL_ROUTER_TYPES.DATABASES: {
         const navInfo: NavInfo = {
-          navTitle: navTrans('collection'),
+          navTitle: navTrans('database'),
           backPath: '',
           showDatabaseSelector: true,
         };
@@ -48,8 +48,8 @@ export const useNavigationHook = (
       case ALL_ROUTER_TYPES.COLLECTION_DETAIL: {
         const navInfo: NavInfo = {
           navTitle: collectionName,
-          backPath: '/collections',
-          showDatabaseSelector: false,
+          backPath: '',
+          showDatabaseSelector: true,
         };
         setNavInfo(navInfo);
         break;

+ 8 - 18
client/src/hooks/Query.ts

@@ -140,18 +140,21 @@ export const useQuery = (params: {
   // reset
   const reset = () => {
     setCurrentPage(0);
-    setQueryResult({ data: [] });
+    setQueryResult({ data: [], latency: 0 });
     pageCache.current.clear();
   };
 
   // Get fields at first or collection name changed.
   useEffect(() => {
+    // reset
+    reset();
+    // get collection info
     params.collectionName && prepare(params.collectionName);
   }, [params.collectionName]);
 
   // query if expr is changed
   useEffect(() => {
-    if (!collection || !collection!.loaded) {
+    if (!collection || !collection.loaded) {
       // console.info('[skip running query]: no key yet');
       return;
     } // reset
@@ -160,34 +163,21 @@ export const useQuery = (params: {
     count();
     // do the query
     query();
-  }, [expr]);
+  }, [expr, pageSize]);
 
   // query if collection is changed
   useEffect(() => {
-    if (!collection || !collection!.loaded) {
+    if (!collection || !collection.loaded) {
       // console.info('[skip running query]: no key yet');
       return;
     }
-    // reset
-    reset();
+
     // get count;
     count();
     // do the query
     query();
   }, [collection]);
 
-  // query if page size is changed
-  useEffect(() => {
-    if (!collection || !collection!.loaded) {
-      // console.info('[skip running query]: no key yet');
-      return;
-    }
-    // reset
-    reset();
-    // do the query
-    query();
-  }, [pageSize]);
-
   return {
     // collection info(primaryKey, consistency level, fields)
     collection,

+ 1 - 1
client/src/http/Database.service.ts

@@ -1,7 +1,7 @@
 import {
   CreateDatabaseParams,
   DropDatabaseParams,
-} from '../pages/database/Types';
+} from '../pages/dbAdmin/Types';
 import BaseModel from './BaseModel';
 
 export class DatabaseService extends BaseModel {

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

@@ -83,7 +83,7 @@ const collectionTrans = {
   // collection tabs
   partitionTab: '分区',
   schemaTab: 'Schema',
-  queryTab: '数据',
+  dataTab: '数据',
   previewTab: '数据预览',
   segmentsTab: '数据段(Segments)',
   startTip: '开始你的数据查询',

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

@@ -5,7 +5,8 @@ const navTrans = {
   search: '向量搜索',
   system: '系统视图',
   user: '用户',
-  database: '数据库管理',
+  dbAdmin: '数据库管理',
+  database: '数据库',
 };
 
 export default navTrans;

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

@@ -85,7 +85,7 @@ const collectionTrans = {
   // collection tabs
   partitionTab: 'Partitions',
   schemaTab: 'Schema',
-  queryTab: 'Data',
+  dataTab: 'Data',
   previewTab: 'Data Preview',
   segmentsTab: 'Segments',
   startTip: 'Start Your Data Query',

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

@@ -5,7 +5,8 @@ const navTrans = {
   search: 'Vector Search',
   system: 'System View',
   user: 'User',
-  database: 'Database Management',
+  dbAdmin: 'Database Management',
+  database: 'Database',
 };
 
 export default navTrans;

+ 0 - 89
client/src/pages/collections/Collection.tsx

@@ -1,89 +0,0 @@
-import { useContext } from 'react';
-import { useParams } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-import { makeStyles, Theme } from '@material-ui/core';
-import { authContext } from '@/context';
-import { useNavigationHook } from '@/hooks';
-import { ALL_ROUTER_TYPES } from '@/router/Types';
-import RouteTabList from '@/components/customTabList/RouteTabList';
-import { ITab } from '@/components/customTabList/Types';
-import Partitions from '../partitions/Partitions';
-import Schema from '../schema/Schema';
-import Query from '../query/Query';
-import Segments from '../segments/Segments';
-
-const useStyles = makeStyles((theme: Theme) => ({
-  wrapper: {
-    flexDirection: 'row',
-    gap: theme.spacing(2),
-  },
-  card: {
-    boxShadow: 'none',
-    flexBasis: theme.spacing(28),
-    width: theme.spacing(28),
-    flexGrow: 0,
-    flexShrink: 0,
-  },
-  tab: {
-    flexGrow: 1,
-    flexShrink: 1,
-    overflowX: 'auto',
-  },
-}));
-
-const Collection = () => {
-  const classes = useStyles();
-  const { isManaged } = useContext(authContext);
-
-  const { collectionName = '', tab = '' } = useParams<{
-    collectionName: string;
-    tab: string;
-  }>();
-
-  useNavigationHook(ALL_ROUTER_TYPES.COLLECTION_DETAIL, { collectionName });
-
-  const { t: collectionTrans } = useTranslation('collection');
-
-  const tabs: ITab[] = [
-    {
-      label: collectionTrans('queryTab'),
-      component: <Query />,
-      path: `query`,
-    },
-    {
-      label: collectionTrans('schemaTab'),
-      component: <Schema />,
-      path: `schema`,
-    },
-    {
-      label: collectionTrans('partitionTab'),
-      component: <Partitions />,
-      path: `partitions`,
-    },
-
-    {
-      label: collectionTrans('segmentsTab'),
-      component: <Segments />,
-      path: `segments`,
-    },
-  ];
-
-  // exclude parititon on cloud
-  if (isManaged) {
-    tabs.splice(1, 1);
-  }
-
-  const activeTab = tabs.findIndex(t => t.path === tab);
-
-  return (
-    <section className={`page-wrapper ${classes.wrapper}`}>
-      <RouteTabList
-        tabs={tabs}
-        wrapperClass={classes.tab}
-        activeIndex={activeTab !== -1 ? activeTab : 0}
-      />
-    </section>
-  );
-};
-
-export default Collection;

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

@@ -25,9 +25,14 @@ import ImportSampleDialog from '../dialogs/ImportSampleDialog';
 import { LOADING_STATE } from '@/consts';
 import { formatNumber } from '@/utils';
 import Aliases from './Aliases';
-import { CollectionObject, CollectionFullObject } from '@server/types';
+import { CollectionObject } from '@server/types';
 
 const useStyles = makeStyles((theme: Theme) => ({
+  root: {
+    display: 'flex',
+    flexDirection: 'column',
+    height: `100%`,
+  },
   emptyWrapper: {
     marginTop: theme.spacing(2),
   },
@@ -65,7 +70,7 @@ const useStyles = makeStyles((theme: Theme) => ({
 const Collections = () => {
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
   const { isManaged } = useContext(authContext);
-  const { database, collections, setCollections, loading, fetchCollections } =
+  const { collections, database, loading, fetchCollections } =
     useContext(dataContext);
 
   const [searchParams] = useSearchParams();
@@ -375,7 +380,7 @@ const Collections = () => {
       formatter({ collection_name }) {
         return (
           <Link
-            to={`/collections/${collection_name}/data`}
+            to={`/databases/${database}/${collection_name}/data`}
             className={classes.link}
             title={collection_name}
           >
@@ -594,7 +599,7 @@ const Collections = () => {
   const CollectionIcon = icons.navCollection;
 
   return (
-    <section className="page-wrapper">
+    <section className={classes.root}>
       {collections.length > 0 || loading ? (
         <AttuGrid
           toolbarConfigs={toolbarConfigs}

+ 202 - 0
client/src/pages/databases/Databases.tsx

@@ -0,0 +1,202 @@
+import { useEffect, useState, useCallback, useRef, useContext } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme } from '@material-ui/core';
+import { useNavigationHook } from '@/hooks';
+import { ALL_ROUTER_TYPES } from '@/router/Types';
+import RouteTabList from '@/components/customTabList/RouteTabList';
+import CustomTree, {
+  CustomTreeItem,
+  TreeNodeType,
+} from '@/components/CustomTree';
+import { ITab } from '@/components/customTabList/Types';
+import Partitions from '../partitions/Partitions';
+import Schema from '../schema/Schema';
+import Query from '../query/Query';
+import Segments from '../segments/Segments';
+import { dataContext } from '@/context';
+import Collections from '../collections/Collections';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    flexDirection: 'row',
+    gap: theme.spacing(2),
+  },
+  card: {
+    boxShadow: 'none',
+    flexBasis: theme.spacing(28),
+    width: theme.spacing(28),
+    flexGrow: 0,
+    flexShrink: 0,
+  },
+  tab: {
+    flexGrow: 1,
+    flexShrink: 1,
+    overflowX: 'auto',
+  },
+}));
+
+const Databases = () => {
+  // UI stats
+  const [localLoading, setLoading] = useState(false);
+  const [collectionsTree, setCollectionsTree] = useState<CustomTreeItem>({
+    id: '',
+    name: '',
+    type: 'db',
+    children: [],
+  });
+  const [selectedTarget, setSelectedTarget] = useState<string[]>();
+
+  // get current collection from url
+  const {
+    databaseItem = '',
+    databaseName = '',
+    collectionName = '',
+    collectionItem = '',
+  } = useParams();
+  // get navigate
+  const navigate = useNavigate();
+  // update navigation
+  useNavigationHook(ALL_ROUTER_TYPES.COLLECTION_DETAIL, { collectionName });
+  // get style
+  const classes = useStyles();
+  // get global data
+  const { database, collections, loading } = useContext(dataContext);
+
+  // i18n
+  const { t: collectionTrans } = useTranslation('collection');
+
+  // fetch data callback
+  const refresh = useCallback(async () => {
+    try {
+      // set UI loading
+      setLoading(true);
+
+      // format tree data
+      const children = collections.map(c => {
+        return {
+          id: c.collection_name,
+          name: c.collection_name,
+          type: 'collection' as TreeNodeType,
+        };
+      });
+      // update tree
+      setCollectionsTree({
+        id: database,
+        name: database,
+        expanded: children.length > 0,
+        type: 'db',
+        children: children,
+      });
+    } finally {
+      setLoading(false);
+    }
+  }, [setCollectionsTree, database, collections]);
+
+  // const onNodeToggle = (event: React.ChangeEvent<{}>, nodeIds: string[]) => {
+  //   console.log('onNodeToggle', event, nodeIds);
+  // };
+
+  const onNodeClick = (node: CustomTreeItem) => {
+    navigate(
+      node.type === 'db'
+        ? `/databases/${database}/${databaseItem || 'collections'}`
+        : `/databases/${database}/${node.id}/${collectionItem || 'data'}`
+    );
+  };
+
+  // change database should go to it's page
+  const firstRender = useRef(false);
+
+  // change database should go to it's page
+  useEffect(() => {
+    if (firstRender.current) {
+      navigate(`/databases/${database}/${databaseItem || 'collections'}`);
+    } else {
+      firstRender.current = true;
+    }
+  }, [database]);
+
+  // fetch data
+  useEffect(() => {
+    refresh();
+  }, [refresh]);
+
+  // active default selected
+  useEffect(() => {
+    setSelectedTarget(
+      collectionName ? [collectionName] : [databaseName || database]
+    );
+  }, [collectionName, database, databaseName]);
+
+  const dbTab: ITab[] = [
+    {
+      label: collectionTrans('collections'),
+      component: <Collections />,
+      path: `collections`,
+    },
+  ];
+  const actionDbTab = dbTab.findIndex(t => t.path === databaseName);
+
+  // collection tabs
+  const collectionTabs: ITab[] = [
+    {
+      label: collectionTrans('dataTab'),
+      component: <Query />,
+      path: `data`,
+    },
+    {
+      label: collectionTrans('schemaTab'),
+      component: <Schema />,
+      path: `schema`,
+    },
+    {
+      label: collectionTrans('partitionTab'),
+      component: <Partitions />,
+      path: `partitions`,
+    },
+
+    {
+      label: collectionTrans('segmentsTab'),
+      component: <Segments />,
+      path: `segments`,
+    },
+  ];
+  // get active collection tab
+  const activeColTab = collectionTabs.findIndex(t => t.path === collectionItem);
+
+  // render
+  const uiLoading = localLoading || loading;
+  return (
+    <section className={`page-wrapper ${classes.wrapper}`}>
+      <section className={classes.card}>
+        {uiLoading ? (
+          `loading`
+        ) : (
+          <CustomTree
+            key="collections"
+            data={collectionsTree}
+            defaultSelected={selectedTarget}
+            onNodeClick={onNodeClick}
+          />
+        )}
+      </section>
+      {!collectionName && (
+        <RouteTabList
+          tabs={dbTab}
+          wrapperClass={classes.tab}
+          activeIndex={actionDbTab !== -1 ? actionDbTab : 0}
+        />
+      )}
+      {collectionName && (
+        <RouteTabList
+          tabs={collectionTabs}
+          wrapperClass={classes.tab}
+          activeIndex={activeColTab !== -1 ? activeColTab : 0}
+        />
+      )}
+    </section>
+  );
+};
+
+export default Databases;

+ 0 - 0
client/src/pages/database/Create.tsx → client/src/pages/dbAdmin/Create.tsx


+ 0 - 0
client/src/pages/database/Database.tsx → client/src/pages/dbAdmin/Database.tsx


+ 0 - 0
client/src/pages/database/Types.ts → client/src/pages/dbAdmin/Types.ts


+ 17 - 12
client/src/pages/index.tsx

@@ -7,7 +7,12 @@ import Header from '@/components/layout/Header';
 import NavMenu from '@/components/menu/NavMenu';
 import { NavMenuItem } from '@/components/menu/Types';
 import icons from '@/components/icons/Icons';
-import { authContext, rootContext, prometheusContext } from '@/context';
+import {
+  authContext,
+  rootContext,
+  prometheusContext,
+  dataContext,
+} from '@/context';
 import Overview from '@/pages/overview/Overview';
 
 const useStyles = makeStyles((theme: Theme) =>
@@ -45,14 +50,18 @@ function Index() {
   const navigate = useNavigate();
   const { isAuth, isManaged } = useContext(authContext);
   const { isPrometheusReady } = useContext(prometheusContext);
+  const { database } = useContext(dataContext);
   const { versionInfo } = useContext(rootContext);
   const { t: navTrans } = useTranslation('nav');
   const classes = useStyles();
   const location = useLocation();
   const isIndex = location.pathname === '/';
   const defaultActive = useMemo(() => {
-    if (location.pathname.includes('collection')) {
-      return navTrans('collection');
+    if (location.pathname.includes('databases')) {
+      return navTrans('databases');
+    }
+    if (location.pathname.includes('db-admin')) {
+      return navTrans('db-admin');
     }
 
     if (location.pathname.includes('search')) {
@@ -67,10 +76,6 @@ function Index() {
       return navTrans('user');
     }
 
-    if (location.pathname.includes('db-admin')) {
-      return navTrans('database');
-    }
-
     return navTrans('overview');
   }, [location, navTrans]);
 
@@ -81,9 +86,9 @@ function Index() {
       onClick: () => navigate('/'),
     },
     {
-      icon: icons.navCollection,
-      label: navTrans('collection'),
-      onClick: () => navigate('/collections'),
+      icon: icons.database,
+      label: navTrans('databases'),
+      onClick: () => navigate(`/databases/${database}`),
     },
     {
       icon: icons.navSearch,
@@ -110,8 +115,8 @@ function Index() {
         iconNormalClass: 'active',
       },
       {
-        icon: icons.database,
-        label: navTrans('database'),
+        icon: icons.settings,
+        label: navTrans('db-admin'),
         onClick: () => navigate('/db-admin'),
       }
     );

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

@@ -160,7 +160,10 @@ const CollectionCard: FC<CollectionCardProps> = ({
         <div>
           <Status status={status} percentage={loadedPercentage} />
         </div>
-        <Link className="link" to={`/collections/${collection_name}/data`}>
+        <Link
+          className="link"
+          to={`/databases/${database}/${collection_name}/data`}
+        >
           {collection_name}
           <RightArrowIcon classes={{ root: classes.icon }} />
         </Link>

+ 1 - 1
client/src/pages/segments/Segments.tsx

@@ -151,7 +151,7 @@ const Segments = () => {
 
   useEffect(() => {
     fetchSegments();
-  }, []);
+  }, [collectionName]);
 
   const {
     pageSize,

+ 14 - 9
client/src/router/Router.tsx

@@ -1,11 +1,10 @@
 import { HashRouter as Router, Routes, Route } from 'react-router-dom';
 import { useContext } from 'react';
 import { authContext } from '@/context';
-import Collection from '@/pages/collections/Collection';
-import Collections from '@/pages/collections/Collections';
+import Databases from '@/pages/databases/Databases';
 import Connect from '@/pages/connect/Connect';
 import Users from '@/pages/user/Users';
-import Database from '@/pages/database/Database';
+import DatabaseAdmin from '@/pages/dbAdmin/Database';
 import Index from '@/pages/index';
 import Search from '@/pages/search/VectorSearch';
 import System from '@/pages/system/SystemView';
@@ -18,13 +17,19 @@ const RouterComponent = () => {
     <Router>
       <Routes>
         <Route path="/" element={<Index />}>
-          <Route index element={<Database />} />
-          <Route path="db-admin" element={<Database />} />
-          <Route path="collections" element={<Collections />} />
-          <Route path="collections/:collectionName" element={<Collection />} />
+          <Route index element={<Databases />} />
+          <Route path="db-admin" element={<DatabaseAdmin />} />
+
+          <Route path="databases" element={<Databases />} />
+          <Route path="databases/:databaseName" element={<Databases />} />
+          <Route
+            path="databases/:databaseName/:databaseItem"
+            element={<Databases />}
+          />
+
           <Route
-            path="collections/:collectionName/:tab"
-            element={<Collection />}
+            path="databases/:databaseName/:collectionName/:collectionItem"
+            element={<Databases />}
           />
 
           <Route path="search" element={<Search />} />

+ 10 - 2
client/src/utils/Validation.ts

@@ -1,6 +1,6 @@
 import { ChildrenStatusType } from '@/components/status/Types';
 import { MetricType, METRIC_TYPES_VALUES } from '@/consts';
-import { Collection } from '@/http';
+import { CollectionObject } from '@server/types';
 
 export type ValidType =
   | 'email'
@@ -265,7 +265,7 @@ export const getCheckResult = (param: ICheckMapParam): boolean => {
 /**
  * Check collection is loading or not
  */
-export const checkLoading = (v: Collection): boolean =>
+export const checkLoading = (v: CollectionObject): boolean =>
   v.loadedPercentage !== '-1' && v.loadedPercentage !== '100';
 
 /**
@@ -275,3 +275,11 @@ export const checkLoading = (v: Collection): boolean =>
  */
 export const checkIndexBuilding = (v: any): boolean =>
   v._indexState === ChildrenStatusType.CREATING;
+
+// get database name from url
+export const getDbValueFromUrl = (currentUrl: string) => {
+  const url = new URL(currentUrl);
+  const pathname = url.hash;
+  const match = pathname.match(/\/databases\/([^/]+)/);
+  return match ? match[1] : null;
+};