Browse Source

new attu Home (#436)

* home part1

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

* home part2

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

* add delete button

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

* support view and manage database on homepage

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

* home page part2

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

* add loadings

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
e9e4fd51a9

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

@@ -95,7 +95,7 @@ const Header: FC<HeaderType> = props => {
     await MilvusService.useDatabase({ database });
   };
 
-  const dbOptions = databases.map(d => ({ value: d, label: d }));
+  const dbOptions = databases.map(d => ({ value: d.name, label: d.name }));
   return (
     <header className={classes.header}>
       <div className={classes.contentWrapper}>

+ 1 - 1
client/src/components/menu/NavMenu.tsx

@@ -85,7 +85,7 @@ const useStyles = makeStyles((theme: Theme) =>
     },
     logoExpand: {
       marginRight: theme.spacing(1),
-      transform: 'scale(2)',
+      transform: 'scale(1.5)',
     },
     logoCollapse: {
       transform: 'scale(1.5)',

+ 49 - 14
client/src/context/Data.tsx

@@ -13,19 +13,28 @@ import { IndexCreateParam, IndexManageParam } from '@/pages/schema/Types';
 import { getDbValueFromUrl } from '@/utils';
 import { DataContextType } from './Types';
 import { LAST_TIME_DATABASE } from '@/consts';
-import { CollectionObject, CollectionFullObject } from '@server/types';
+import {
+  CollectionObject,
+  CollectionFullObject,
+  DatabaseObject,
+} from '@server/types';
 import { WS_EVENTS, WS_EVENTS_TYPE } from '@server/utils/Const';
 import { checkIndexing, checkLoading } from '@server/utils/Shared';
 
 export const dataContext = createContext<DataContextType>({
-  loading: false,
+  loading: true,
+  loadingDatabases: true,
   collections: [],
   setCollections: () => {},
-  database: 'default',
+  database: '',
   setDatabase: () => {},
   databases: [],
   setDatabaseList: () => {},
-  fetchDatabases: async () => {},
+  createDatabase: async () => {},
+  dropDatabase: async () => {},
+  fetchDatabases: async () => {
+    return [];
+  },
   fetchCollections: async () => {},
   fetchCollection: async () => {
     return {} as CollectionFullObject;
@@ -69,15 +78,16 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
   // local data state
   const [collections, setCollections] = useState<CollectionObject[]>([]);
   const [connected, setConnected] = useState(false);
-  const [loading, setLoading] = useState(false);
-  const [database, setDatabase] = useState<string>(
+  const [loading, setLoading] = useState(true);
+  const [loadingDatabases, setLoadingDatabases] = useState(true);
+  const defaultDb =
     initialDatabase ||
-      window.localStorage.getItem(LAST_TIME_DATABASE) ||
-      'default'
-  );
-  const [databases, setDatabases] = useState<string[]>([database]);
+    window.localStorage.getItem(LAST_TIME_DATABASE) ||
+    'default';
+  const [database, setDatabase] = useState<string>(defaultDb);
+  const [databases, setDatabases] = useState<DatabaseObject[]>([]);
   // auth context
-  const { isAuth, clientId } = useContext(authContext);
+  const { isAuth, clientId, logout } = useContext(authContext);
   // socket ref
   const socket = useRef<Socket | null>(null);
 
@@ -134,9 +144,31 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
 
   // API: fetch databases
   const fetchDatabases = async () => {
-    const res = await DatabaseService.getDatabases();
+    setLoadingDatabases(true);
+    const newDatabases = await DatabaseService.listDatabases();
+    setLoadingDatabases(false);
+
+    // if no database, logout
+    if (newDatabases.length === 0) {
+      logout();
+    }
+    setDatabases(newDatabases);
+
+    return newDatabases;
+  };
+
+  // API: create database
+  const createDatabase = async (params: { db_name: string }) => {
+    await DatabaseService.createDatabase(params);
+    await fetchDatabases();
+  };
+
+  // API: delete database
+  const dropDatabase = async (params: { db_name: string }) => {
+    await DatabaseService.dropDatabase(params);
+    const newDatabases = await fetchDatabases();
 
-    setDatabases(res.db_names);
+    setDatabase(newDatabases[0].name);
   };
 
   // API:fetch collections
@@ -308,7 +340,7 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
       // clear collections
       setCollections([]);
       // clear database
-      setDatabases(['default']);
+      setDatabases([]);
       // set connected to false
       setConnected(false);
     }
@@ -337,12 +369,15 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
     <Provider
       value={{
         loading,
+        loadingDatabases,
         collections,
         setCollections,
         database,
         databases,
         setDatabase,
         setDatabaseList: setDatabases,
+        createDatabase,
+        dropDatabase,
         fetchDatabases,
         fetchCollections,
         fetchCollection,

+ 16 - 4
client/src/context/Types.ts

@@ -1,5 +1,9 @@
 import { Dispatch, ReactElement, SetStateAction } from 'react';
-import { CollectionObject, CollectionFullObject } from '@server/types';
+import {
+  CollectionObject,
+  CollectionFullObject,
+  DatabaseObject,
+} from '@server/types';
 import { NavInfo } from '@/router/Types';
 import { IndexCreateParam, IndexManageParam } from '@/pages/schema/Types';
 
@@ -91,13 +95,21 @@ export type NavContextType = {
 
 export type DataContextType = {
   loading: boolean;
+  loadingDatabases: boolean;
   collections: CollectionObject[];
   setCollections: Dispatch<SetStateAction<CollectionObject[]>>;
   database: string;
   setDatabase: Dispatch<SetStateAction<string>>;
-  databases: string[];
-  setDatabaseList: Dispatch<SetStateAction<string[]>>;
-  fetchDatabases: () => Promise<void>;
+  databases: DatabaseObject[];
+  setDatabaseList: Dispatch<SetStateAction<DatabaseObject[]>>;
+
+  // APIs
+  // databases
+  fetchDatabases: () => Promise<DatabaseObject[]>;
+  createDatabase: (params: { db_name: string }) => Promise<void>;
+  dropDatabase: (params: { db_name: string }) => Promise<void>;
+
+  // collections
   fetchCollections: () => Promise<void>;
   fetchCollection: (name: string) => Promise<CollectionFullObject>;
   createCollection: (data: any) => Promise<CollectionFullObject>;

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

@@ -20,9 +20,9 @@ export const useNavigationHook = (
     switch (type) {
       case ALL_ROUTER_TYPES.OVERVIEW: {
         const navInfo: NavInfo = {
-          navTitle: navTrans('overview'),
+          navTitle: navTrans('welcome'),
           backPath: '',
-          showDatabaseSelector: true,
+          showDatabaseSelector: false,
         };
         setNavInfo(navInfo);
         break;

+ 12 - 6
client/src/http/Database.service.ts

@@ -1,12 +1,18 @@
-import {
-  CreateDatabaseParams,
-  DropDatabaseParams,
-} from '../pages/dbAdmin/Types';
 import BaseModel from './BaseModel';
+import { DatabaseObject } from '@server/types';
+
+// request types
+export interface CreateDatabaseParams {
+  db_name: string;
+}
+
+export interface DropDatabaseParams {
+  db_name: string;
+}
 
 export class DatabaseService extends BaseModel {
-  static getDatabases() {
-    return super.search<{ db_names: [] }>({
+  static listDatabases() {
+    return super.search<DatabaseObject[]>({
       path: `/databases`,
       params: {},
     });

+ 5 - 0
client/src/i18n/cn/home.ts

@@ -0,0 +1,5 @@
+const homeTrans = {
+  welcome: '欢迎来到 Milvus!',
+};
+
+export default homeTrans;

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

@@ -1,5 +1,6 @@
 const navTrans = {
   overview: '概览',
+  welcome: '欢迎来到 Milvus!',
   collection: 'Collection',
   console: '搜索控制台',
   search: '向量搜索',

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

@@ -18,6 +18,7 @@ const overviewTrans = {
   dataNodes: '数据节点',
   queryNodes: '查询节点',
   indexNodes: '索引节点',
+  createdTime: '创建时间',
 };
 
 export default overviewTrans;

+ 5 - 0
client/src/i18n/en/home.ts

@@ -0,0 +1,5 @@
+const homeTrans = {
+  welcome: 'Welcome to Milvus!',
+};
+
+export default homeTrans;

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

@@ -1,5 +1,6 @@
 const navTrans = {
   overview: 'Overview',
+  welcome: 'Welcome to Milvus!',
   collection: 'Collection',
   console: 'Search Console',
   search: 'Vector Search',

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

@@ -1,13 +1,13 @@
 const overviewTrans = {
   load: 'Loaded Collections',
-  all: 'All Collections',
+  all: 'Collections',
   data: 'Approx Entities',
   rows: '{{number}}',
   loading: 'Loading Collections',
   sysInfo: 'System Info',
   database: 'Database',
   milvusVersion: 'Milvus Version',
-  upTime: 'Root Coordinator Up Time',
+  upTime: 'Up Time',
   deployMode: 'Deploy Mode',
   databases: 'Databases',
   users: 'Users',
@@ -18,6 +18,7 @@ const overviewTrans = {
   dataNodes: 'Data Nodes',
   queryNodes: 'Query Nodes',
   indexNodes: 'Index Nodes',
+  createdTime: 'Created Time',
 };
 
 export default overviewTrans;

+ 3 - 1
client/src/pages/databases/Databases.tsx

@@ -13,6 +13,8 @@ import Query from '../query/Query';
 import Segments from '../segments/Segments';
 import { dataContext } from '@/context';
 import Collections from '../collections/Collections';
+import StatusIcon from '@/components/status/StatusIcon';
+import { ChildrenStatusType } from '@/components/status/Types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -97,7 +99,7 @@ const Databases = () => {
     <section className={`page-wrapper ${classes.wrapper}`}>
       <section className={classes.tree}>
         {loading ? (
-          `loading`
+          <StatusIcon type={ChildrenStatusType.CREATING} />
         ) : (
           <DatabaseTree
             key="collections"

+ 0 - 139
client/src/pages/dbAdmin/Database.tsx

@@ -1,139 +0,0 @@
-import { useContext, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { DatabaseService } from '@/http';
-import AttuGrid from '@/components/grid/Grid';
-import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
-import {
-  CreateDatabaseParams,
-  DropDatabaseParams,
-  DatabaseData,
-} from './Types';
-import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
-import { rootContext, dataContext } from '@/context';
-import { useNavigationHook } from '@/hooks';
-import { ALL_ROUTER_TYPES } from '@/router/Types';
-import CreateDatabaseDialog from './Create';
-
-const DatabaseAdminPage = () => {
-  useNavigationHook(ALL_ROUTER_TYPES.DB_ADMIN);
-  const { databases, fetchDatabases } = useContext(dataContext);
-
-  const [selectedDatabase, setSelectedDatabase] = useState<DatabaseData[]>([]);
-  const { setDialog, handleCloseDialog, openSnackBar } =
-    useContext(rootContext);
-  const { t: successTrans } = useTranslation('success');
-  const { t: dbTrans } = useTranslation('database');
-  const { t: btnTrans } = useTranslation('btn');
-  const { t: dialogTrans } = useTranslation('dialog');
-
-  const handleCreate = async (data: CreateDatabaseParams) => {
-    await DatabaseService.createDatabase(data);
-    fetchDatabases();
-    openSnackBar(successTrans('create', { name: dbTrans('database') }));
-    handleCloseDialog();
-  };
-
-  const handleDelete = async () => {
-    for (const db of selectedDatabase) {
-      const param: DropDatabaseParams = {
-        db_name: db.name,
-      };
-      await DatabaseService.dropDatabase(param);
-    }
-
-    openSnackBar(successTrans('delete', { name: dbTrans('database') }));
-    fetchDatabases();
-    setSelectedDatabase([]);
-    handleCloseDialog();
-  };
-
-  const toolbarConfigs: ToolBarConfig[] = [
-    {
-      label: dbTrans('database'),
-      onClick: () => {
-        setDialog({
-          open: true,
-          type: 'custom',
-          params: {
-            component: (
-              <CreateDatabaseDialog
-                handleCreate={handleCreate}
-                handleClose={handleCloseDialog}
-              />
-            ),
-          },
-        });
-      },
-      icon: 'add',
-    },
-
-    {
-      type: 'button',
-      btnVariant: 'text',
-      btnColor: 'secondary',
-      onClick: () => {
-        setDialog({
-          open: true,
-          type: 'custom',
-          params: {
-            component: (
-              <DeleteTemplate
-                label={btnTrans('drop')}
-                title={dialogTrans('deleteTitle', {
-                  type: dbTrans('database'),
-                })}
-                text={dbTrans('deleteWarning')}
-                handleDelete={handleDelete}
-              />
-            ),
-          },
-        });
-      },
-      label: btnTrans('drop'),
-      disabled: () =>
-        selectedDatabase.length === 0 ||
-        selectedDatabase.findIndex(v => v.name === 'default') > -1,
-      disabledTooltip: dbTrans('deleteTip'),
-
-      icon: 'delete',
-    },
-  ];
-
-  const colDefinitions: ColDefinitionsType[] = [
-    {
-      id: 'name',
-      align: 'left',
-      disablePadding: false,
-      label: 'Name',
-    },
-  ];
-
-  const handleSelectChange = (value: DatabaseData[]) => {
-    setSelectedDatabase(value);
-  };
-
-  return (
-    <div className="page-wrapper">
-      <AttuGrid
-        toolbarConfigs={toolbarConfigs}
-        colDefinitions={colDefinitions}
-        rows={databases.map(d => ({ name: d }))}
-        rowCount={databases.length}
-        primaryKey="name"
-        showPagination={false}
-        selected={selectedDatabase}
-        setSelected={handleSelectChange}
-        // page={currentPage}
-        // onPageChange={handlePageChange}
-        // rowsPerPage={pageSize}
-        // setRowsPerPage={handlePageSize}
-        // isLoading={loading}
-        // order={order}
-        // orderBy={orderBy}
-        // handleSort={handleGridSort}
-      />
-    </div>
-  );
-};
-
-export default DatabaseAdminPage;

+ 0 - 15
client/src/pages/dbAdmin/Types.ts

@@ -1,15 +0,0 @@
-export interface DatabaseData {
-  name: string;
-}
-export interface CreateDatabaseParams {
-  db_name: string;
-}
-
-export interface CreateDatabaseProps {
-  handleCreate: (data: CreateDatabaseParams) => void;
-  handleClose: () => void;
-}
-
-export interface DropDatabaseParams {
-  db_name: string;
-}

+ 36 - 10
client/src/pages/dbAdmin/Create.tsx → client/src/pages/dialogs/CreateDatabaseDialog.tsx

@@ -1,12 +1,13 @@
 import { makeStyles, Theme } from '@material-ui/core';
-import { FC, useMemo, useState } from 'react';
+import { FC, useMemo, useState, useContext } from 'react';
 import { useTranslation } from 'react-i18next';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import CustomInput from '@/components/customInput/CustomInput';
 import { ITextfieldConfig } from '@/components/customInput/Types';
 import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
-import { CreateDatabaseProps, CreateDatabaseParams } from './Types';
+import { CreateDatabaseParams } from '@/http';
+import { dataContext, rootContext } from '@/context';
 
 const useStyles = makeStyles((theme: Theme) => ({
   input: {
@@ -14,17 +15,29 @@ const useStyles = makeStyles((theme: Theme) => ({
   },
 }));
 
-const CreateDatabaseDialog: FC<CreateDatabaseProps> = ({
-  handleCreate,
-  handleClose,
-}) => {
+export interface CreateDatabaseProps {
+  onCreate?: () => void;
+}
+
+const CreateDatabaseDialog: FC<CreateDatabaseProps> = ({ onCreate }) => {
+  // context
+  const { createDatabase } = useContext(dataContext);
+  const { openSnackBar, handleCloseDialog } = useContext(rootContext);
+
+  // i18n
   const { t: databaseTrans } = useTranslation('database');
   const { t: btnTrans } = useTranslation('btn');
   const { t: warningTrans } = useTranslation('warning');
+  const { t: successTrans } = useTranslation('success');
+  const { t: dbTrans } = useTranslation('database');
 
+  // UI state
   const [form, setForm] = useState<CreateDatabaseParams>({
     db_name: '',
   });
+  const [loading, setLoading] = useState(false);
+
+  // validation
   const checkedForm = useMemo(() => {
     return formatForm(form);
   }, [form]);
@@ -57,8 +70,21 @@ const CreateDatabaseDialog: FC<CreateDatabaseProps> = ({
     },
   ];
 
-  const handleCreateDatabase = () => {
-    handleCreate(form);
+  const handleCreate = async () => {
+    setLoading(true);
+    await createDatabase(form);
+    openSnackBar(successTrans('create', { name: dbTrans('database') }));
+    setLoading(false);
+
+    handleCloseDialog();
+
+    if (onCreate) {
+      onCreate();
+    }
+  };
+
+  const handleClose = () => {
+    handleCloseDialog();
   };
 
   return (
@@ -66,8 +92,8 @@ const CreateDatabaseDialog: FC<CreateDatabaseProps> = ({
       title={databaseTrans('createTitle')}
       handleClose={handleClose}
       confirmLabel={btnTrans('create')}
-      handleConfirm={handleCreateDatabase}
-      confirmDisabled={disabled}
+      handleConfirm={handleCreate}
+      confirmDisabled={disabled || loading}
     >
       <>
         {createConfigs.map(v => (

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

@@ -67,7 +67,7 @@ const CreatePartition: FC<PartitionCreateProps> = ({
     };
 
     await PartitionService.managePartition(param);
-    onCreate && onCreate();
+    onCreate && onCreate(collectionName);
     handleCloseDialog();
   };
 

+ 4 - 9
client/src/pages/index.tsx

@@ -60,9 +60,6 @@ function Index() {
     if (location.pathname.includes('databases')) {
       return navTrans('database');
     }
-    if (location.pathname.includes('db-admin')) {
-      return navTrans('dbAdmin');
-    }
 
     if (location.pathname.includes('search')) {
       return navTrans('search');
@@ -72,7 +69,10 @@ function Index() {
       return navTrans('system');
     }
 
-    if (location.pathname.includes('users') || location.pathname.includes('roles')) {
+    if (
+      location.pathname.includes('users') ||
+      location.pathname.includes('roles')
+    ) {
       return navTrans('user');
     }
 
@@ -113,11 +113,6 @@ function Index() {
           isPrometheusReady ? navigate('/system_healthy') : navigate('/system'),
         iconActiveClass: 'normal',
         iconNormalClass: 'active',
-      },
-      {
-        icon: icons.settings,
-        label: navTrans('dbAdmin'),
-        onClick: () => navigate('/db-admin'),
       }
     );
   }

+ 202 - 0
client/src/pages/overview/DatabaseCard.tsx

@@ -0,0 +1,202 @@
+import { FC, useContext } from 'react';
+import { makeStyles, Theme, Typography, useTheme } from '@material-ui/core';
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { MilvusService } from '@/http';
+import icons from '@/components/icons/Icons';
+import CustomButton from '@/components/customButton/CustomButton';
+import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
+import { rootContext } from '@/context';
+import { DatabaseObject } from '@server/types';
+import CreateDatabaseDialog from '../dialogs/CreateDatabaseDialog';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    position: 'relative',
+    display: `flex`,
+    flexDirection: `column`,
+    gap: theme.spacing(1),
+    backgroundColor: theme.palette.common.white,
+    padding: theme.spacing(2),
+    border: '1px solid #E0E0E0',
+    minWidth: '180px',
+    minHeight: '126px',
+    cursor: 'pointer',
+    '&:hover': {
+      boxShadow: '0px 0px 4px 0px #00000029',
+    },
+  },
+  dbTitle: {
+    fontSize: '20px',
+    lineHeight: '24px',
+    fontWeight: 'bold',
+    marginBottom: theme.spacing(1),
+    color: theme.palette.attuDark.main,
+    '& svg': {
+      verticalAlign: '-3px',
+    },
+  },
+  label: {
+    fontSize: '12px',
+    lineHeight: '16px',
+    color: theme.palette.attuDark.main,
+  },
+  value: {
+    fontSize: '24px',
+    lineHeight: '28px',
+    fontWeight: 'bold',
+    marginBottom: theme.spacing(1),
+  },
+  delIcon: {
+    color: theme.palette.attuGrey.main,
+    cursor: 'pointer',
+    position: 'absolute',
+    right: 4,
+    top: 4,
+    minWidth: 0,
+    minHeight: 0,
+    padding: theme.spacing(0.5),
+    '& svg': {
+      width: 15,
+    },
+  },
+
+  // create db
+  create: {
+    border: `1px dashed ${theme.palette.primary.main}`,
+    justifyContent: 'center',
+    alignItems: 'center',
+    color: theme.palette.primary.main,
+  },
+}));
+
+export interface DatabaseCardProps {
+  wrapperClass?: string;
+  database: DatabaseObject;
+  setDatabase: (database: string) => void;
+  dropDatabase: (params: { db_name: string }) => Promise<void>;
+}
+
+const DatabaseCard: FC<DatabaseCardProps> = ({
+  database = { name: '', collections: [], createdTime: 0 },
+  wrapperClass = '',
+  setDatabase,
+  dropDatabase,
+}) => {
+  const { t: overviewTrans } = useTranslation('overview');
+  const { t: successTrans } = useTranslation('success');
+  const { t: dbTrans } = useTranslation('database');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: dialogTrans } = useTranslation('dialog');
+
+  const { setDialog, openSnackBar, handleCloseDialog } =
+    useContext(rootContext);
+
+  const navigation = useNavigate();
+  const classes = useStyles();
+  const theme = useTheme();
+  const DbIcon = icons.database;
+  const DeleteIcon = icons.delete;
+  const PlusIcon = icons.add;
+
+  const onClick = async () => {
+    // use database
+    await MilvusService.useDatabase({ database: database.name });
+    // set database
+    setDatabase(database.name);
+
+    // navigate to database detail page
+    navigation(`/databases/${database.name}`);
+  };
+
+  const handleDelete = async () => {
+    await dropDatabase({ db_name: database.name });
+
+    openSnackBar(successTrans('delete', { name: dbTrans('database') }));
+    handleCloseDialog();
+  };
+
+  // empty database => create new database
+  if (database.name === 'new') {
+    return (
+      <section
+        className={`${wrapperClass} ${classes.wrapper} ${classes.create}`}
+        onClick={() => {
+          setDialog({
+            open: true,
+            type: 'custom',
+            params: {
+              component: <CreateDatabaseDialog />,
+            },
+          });
+        }}
+      >
+        <PlusIcon />
+        <Typography variant="h6">{dbTrans('createTitle')}</Typography>
+      </section>
+    );
+  }
+
+  return (
+    <section className={`${wrapperClass}`}>
+      <section className={`${classes.wrapper}`} onClick={onClick}>
+        <Typography variant="h3" className={classes.dbTitle}>
+          <DbIcon /> {database.name}
+        </Typography>
+        <div>
+          <div key={database.name}>
+            <Typography className={classes.label}>
+              {overviewTrans('all')}
+            </Typography>
+
+            <Typography
+              className={classes.value}
+              style={{ color: theme.palette.primary.main }}
+            >
+              {database.collections.length}
+            </Typography>
+            {database.createdTime !== -1 && (
+              <>
+                <Typography className={classes.label}>
+                  {overviewTrans('createdTime')}
+                </Typography>
+                <Typography className={classes.label}>
+                  {new Date(database.createdTime / 1000000).toLocaleString()}
+                </Typography>
+              </>
+            )}
+          </div>
+          {database.name !== 'default' && (
+            <CustomButton
+              className={classes.delIcon}
+              onClick={(event: any) => {
+                event.stopPropagation();
+                setDialog({
+                  open: true,
+                  type: 'custom',
+                  params: {
+                    component: (
+                      <DeleteTemplate
+                        label={btnTrans('drop')}
+                        title={dialogTrans('deleteTitle', {
+                          type: dbTrans('database'),
+                        })}
+                        text={dbTrans('deleteWarning')}
+                        handleDelete={handleDelete}
+                        compare={database.name}
+                      />
+                    ),
+                  },
+                });
+              }}
+            >
+              <DeleteIcon />
+            </CustomButton>
+          )}
+        </div>
+      </section>
+    </section>
+  );
+};
+
+export default DatabaseCard;

+ 67 - 178
client/src/pages/overview/Overview.tsx

@@ -1,159 +1,50 @@
 import { useContext, useMemo } from 'react';
-import {
-  makeStyles,
-  Theme,
-  Typography,
-  useTheme,
-  Card,
-  CardContent,
-} from '@material-ui/core';
-import { Link } from 'react-router-dom';
+import { makeStyles, Theme, Typography } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import dayjs from 'dayjs';
-import { rootContext, dataContext, systemContext } from '@/context';
-import EmptyCard from '@/components/cards/EmptyCard';
-import icons from '@/components/icons/Icons';
-import { LOADING_STATE, MILVUS_DEPLOY_MODE } from '@/consts';
+import { dataContext, systemContext } from '@/context';
+import { MILVUS_DEPLOY_MODE } from '@/consts';
 import { useNavigationHook } from '@/hooks';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
-import { formatNumber } from '@/utils';
-import CollectionCard from './collectionCard/CollectionCard';
-import StatisticsCard from './statisticsCard/StatisticsCard';
+import DatabaseCard from './DatabaseCard';
+import SysCard from './SysCard';
+import StatusIcon from '@/components/status/StatusIcon';
+import { ChildrenStatusType } from '@/components/status/Types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   overviewContainer: {
-    display: 'flex',
-    flexDirection: 'row',
-    gap: theme.spacing(4),
-  },
-  collectionTitle: {
-    margin: theme.spacing(2, 0),
-    fontSize: 16,
-    fontWeight: 'bold',
-  },
-  cardsWrapper: {
-    display: 'grid',
-    gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
-    gap: theme.spacing(2),
-  },
-  sysCardsWrapper: {
-    display: 'grid',
-    gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
     gap: theme.spacing(2),
+    '& h4': {
+      marginBottom: theme.spacing(2),
+    },
   },
-  h2: {
-    fontWeight: 'bold',
-    fontSize: '22px',
-    margin: theme.spacing(1, 0, 2, 0),
-  },
-  dbWrapper: {
-    width: '55%',
-    order: 1,
-    padding: theme.spacing(1, 0, 0),
-  },
-  emptyCard: {
-    minHeight: '50vh',
-    color: 'transparent',
-  },
-  sysWrapper: {
-    width: '45%',
-    background: 'rgb(239, 239, 239)',
-    padding: theme.spacing(1, 2, 2),
-    order: 1,
+
+  section: {
+    width: '75%',
+    marginBottom: theme.spacing(2),
   },
-  sysCard: {
-    '& p': {
-      fontSize: '24px',
-      margin: 0,
-    },
-    '& h3': {
-      margin: 0,
-      fontSize: '14px',
-      color: theme.palette.attuGrey.dark,
-    },
-    '& a': {
-      textDecoration: 'none',
-      color: '#000',
-    },
+  cardWrapper: {
+    display: 'flex',
+    flexWrap: 'wrap',
+    flexGrow: 0,
+    gap: theme.spacing(2),
   },
 }));
 
-const SysCard = (data: {
-  title: string;
-  count: number | string;
-  des?: string;
-  link?: string;
-}) => {
-  const classes = useStyles();
-
-  const content = (
-    <>
-      <Typography component={'p'}>{data.count}</Typography>
-      <Typography component={'h3'}>{data.title}</Typography>
-      {data.des ? <Typography component={'p'}>{data.des}</Typography> : null}
-    </>
-  );
-
-  return (
-    <Card className={classes.sysCard}>
-      <CardContent>
-        {data.link ? <Link to={data.link}>{content}</Link> : content}
-      </CardContent>
-    </Card>
-  );
-};
-
 const Overview = () => {
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
-  const { database, databases, collections, loading } = useContext(dataContext);
+  const {
+    databases,
+    database,
+    collections,
+    loadingDatabases,
+    setDatabase,
+    dropDatabase,
+  } = useContext(dataContext);
   const { data } = useContext(systemContext);
   const classes = useStyles();
-  const theme = useTheme();
   const { t: overviewTrans } = useTranslation('overview');
-  const { t: collectionTrans } = useTranslation('collection');
-  const { t: successTrans } = useTranslation('success');
-  const { openSnackBar } = useContext(rootContext);
-
-  const loadCollections = collections.filter(
-    c => c.status !== LOADING_STATE.UNLOADED && typeof c.status !== 'undefined'
-  );
-
-  const onRelease = () => {
-    openSnackBar(
-      successTrans('release', { name: collectionTrans('collection') })
-    );
-  };
-
-  const statisticsData = useMemo(() => {
-    return {
-      data: [
-        {
-          label: overviewTrans('load'),
-          value: formatNumber(loadCollections.length),
-          valueColor: '#07d197',
-        },
-        {
-          label: overviewTrans('all'),
-          value: collections.length,
-          valueColor: theme.palette.primary.main,
-        },
-        {
-          label: overviewTrans('data'),
-          value: overviewTrans('rows', {
-            number: formatNumber(
-              collections.reduce(
-                (acc, cur) => acc + Number(cur.rowCount || 0),
-                0
-              )
-            ),
-          }) as string,
-          valueColor: theme.palette.primary.dark,
-        },
-      ],
-    };
-  }, [overviewTrans, loadCollections]);
-
-  const CollectionIcon = icons.navCollection;
+  const { t: databaseTrans } = useTranslation('database');
 
   // calculation diff to the rootCoord create time
   const duration = useMemo(() => {
@@ -190,59 +81,57 @@ const Overview = () => {
 
   return (
     <section className={`page-wrapper  ${classes.overviewContainer}`}>
-      <section className={classes.dbWrapper}>
-        <Typography component={'h2'} className={classes.h2}>
-          {overviewTrans('database')} {database}
-        </Typography>
-
-        <StatisticsCard data={statisticsData.data} />
-        <Typography component={'h4'} className={classes.collectionTitle}>
-          {overviewTrans('load')}
-        </Typography>
-
-        {loadCollections.length > 0 ? (
-          <div className={classes.cardsWrapper}>
-            {loadCollections.map(collection => (
-              <CollectionCard
-                key={collection.id}
-                collection={collection}
-                onRelease={onRelease}
-              />
-            ))}
-          </div>
+      <section className={classes.section}>
+        <Typography variant="h4">{databaseTrans('databases')}</Typography>
+        {loadingDatabases ? (
+          <StatusIcon type={ChildrenStatusType.CREATING} />
         ) : (
-          <EmptyCard
-            loading={loading}
-            wrapperClass={classes.emptyCard}
-            icon={!loading ? <CollectionIcon /> : undefined}
-            text={
-              loading ? overviewTrans('loading') : collectionTrans('noLoadData')
-            }
-          />
+          <div className={classes.cardWrapper}>
+            {databases.map(db => {
+              // if the database is the current database, using client side collections data to avoid more requests
+              if (db.name === database) {
+                db.collections = collections.map(c => c.collection_name);
+              }
+              return (
+                <DatabaseCard
+                  database={db}
+                  setDatabase={setDatabase}
+                  dropDatabase={dropDatabase}
+                  key={db.name}
+                />
+              );
+            })}
+            <DatabaseCard
+              database={{ name: 'new', collections: [], createdTime: 0 }}
+              setDatabase={setDatabase}
+              dropDatabase={dropDatabase}
+              key={'new'}
+            />
+          </div>
         )}
       </section>
 
-      {data?.systemInfo ? (
-        <section className={classes.sysWrapper}>
-          <Typography component={'h2'} className={classes.h2}>
-            {overviewTrans('sysInfo')}
-          </Typography>
-          <div className={classes.sysCardsWrapper}>
+      {data?.systemInfo && (
+        <section className={classes.section}>
+          <Typography variant="h4">{overviewTrans('sysInfo')}</Typography>
+          <div className={classes.cardWrapper}>
             <SysCard
               title={'Milvus Version'}
               count={data?.systemInfo?.build_version}
+              link="system"
             />
+
             <SysCard
               title={overviewTrans('deployMode')}
               count={data?.deployMode}
+              link="system"
             />
-            <SysCard title={overviewTrans('upTime')} count={duration} />
-
             <SysCard
-              title={overviewTrans('databases')}
-              count={databases?.length}
-              link="databases"
+              title={overviewTrans('upTime')}
+              count={duration}
+              link="system"
             />
+
             <SysCard
               title={overviewTrans('users')}
               count={data?.users?.length}
@@ -251,7 +140,7 @@ const Overview = () => {
             <SysCard
               title={overviewTrans('roles')}
               count={data?.roles?.length}
-              link="users?activeIndex=1"
+              link="roles"
             />
 
             {data?.deployMode === MILVUS_DEPLOY_MODE.DISTRIBUTED ? (
@@ -277,7 +166,7 @@ const Overview = () => {
             ) : null}
           </div>
         </section>
-      ) : null}
+      )}
     </section>
   );
 };

+ 58 - 0
client/src/pages/overview/SysCard.tsx

@@ -0,0 +1,58 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { Link } from 'react-router-dom';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  sysCard: {
+    minWidth: 'auto',
+    gap: theme.spacing(1),
+    backgroundColor: theme.palette.common.white,
+    padding: theme.spacing(2),
+    border: '1px solid #E0E0E0',
+    cursor: 'pointer',
+    '&:hover': {
+      boxShadow: '0px 0px 4px 0px #00000029',
+    },
+
+    '& p': {
+      fontSize: '24px',
+      margin: 0,
+    },
+    '& h3': {
+      margin: 0,
+      fontSize: '14px',
+      lineHeight: 1.5,
+      color: theme.palette.attuGrey.dark,
+    },
+    '& a': {
+      textDecoration: 'none',
+      color: '#000',
+    },
+  },
+}));
+
+const SysCard = (data: {
+  title: string;
+  count: number | string;
+  des?: string;
+  link?: string;
+}) => {
+  const classes = useStyles();
+
+  const content = (
+    <>
+      <Typography component={'p'}>{data.count}</Typography>
+      <Typography component={'h3'}>{data.title}</Typography>
+      {data.des ? <Typography component={'p'}>{data.des}</Typography> : null}
+    </>
+  );
+
+  return (
+    <section className={classes.sysCard}>
+      <section>
+        {data.link ? <Link to={data.link}>{content}</Link> : content}
+      </section>
+    </section>
+  );
+};
+
+export default SysCard;

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

@@ -1,54 +0,0 @@
-import {
-  makeStyles,
-  Theme,
-  Typography,
-  Card,
-  CardContent,
-} 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(160px, 1fr))',
-    columnGap: theme.spacing(2),
-  },
-  label: {
-    fontSize: '12px',
-    lineHeight: '16px',
-    color: theme.palette.attuDark.main,
-  },
-  value: {
-    fontSize: '24px',
-    lineHeight: '28px',
-    fontWeight: 'bold',
-  },
-}));
-
-const StatisticsCard: FC<StatisticsCardProps> = ({
-  data = [],
-  wrapperClass = '',
-}) => {
-  const classes = useStyles();
-
-  return (
-    <Card className={`${wrapperClass}`}>
-      <CardContent className={`${classes.wrapper}`}>
-        {data.map(item => (
-          <div key={item.label}>
-            <Typography className={classes.label}>{item.label}</Typography>
-            <Typography
-              className={classes.value}
-              style={{ color: item.valueColor }}
-            >
-              {item.value}
-            </Typography>
-          </div>
-        ))}
-      </CardContent>
-    </Card>
-  );
-};
-
-export default StatisticsCard;

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

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

+ 0 - 2
client/src/router/Router.tsx

@@ -4,7 +4,6 @@ import { authContext } from '@/context';
 import Databases from '@/pages/databases/Databases';
 import Connect from '@/pages/connect/Connect';
 import Users from '@/pages/user/Users';
-import DatabaseAdmin from '@/pages/dbAdmin/Database';
 import Index from '@/pages/index';
 import Search from '@/pages/search/VectorSearch';
 import System from '@/pages/system/SystemView';
@@ -18,7 +17,6 @@ const RouterComponent = () => {
       <Routes>
         <Route path="/" element={<Index />}>
           <Route index element={<Databases />} />
-          <Route path="db-admin" element={<DatabaseAdmin />} />
 
           <Route path="databases" element={<Databases />} />
           <Route path="databases/:databaseName" element={<Databases />} />

+ 5 - 4
server/src/database/databases.controller.ts

@@ -2,6 +2,7 @@ import { NextFunction, Request, Response, Router } from 'express';
 import { dtoValidationMiddleware } from '../middleware/validation';
 import { DatabasesService } from './databases.service';
 import { CreateDatabaseDto } from './dto';
+import { DatabaseObject } from '../types';
 
 export class DatabasesController {
   private databasesService: DatabasesService;
@@ -46,13 +47,13 @@ export class DatabasesController {
   async listDatabases(req: Request, res: Response, next: NextFunction) {
     try {
       const result = await this.databasesService.listDatabase(req.clientId);
-      result.db_names = result.db_names.sort((a: string, b: string) => {
-        if (a === 'default') {
+      result.sort((a: DatabaseObject, b: DatabaseObject) => {
+        if (a.name === 'default') {
           return -1; // 'default' comes before other strings
-        } else if (b === 'default') {
+        } else if (b.name === 'default') {
           return 1; // 'default' comes after other strings
         } else {
-          return a.localeCompare(b); // sort other strings alphabetically
+          return a.name.localeCompare(b.name); // sort other strings alphabetically
         }
       });
       res.send(result);

+ 14 - 9
server/src/database/databases.service.ts

@@ -2,10 +2,10 @@ import {
   CreateDatabaseRequest,
   ListDatabasesRequest,
   DropDatabasesRequest,
-  ListDatabasesResponse,
 } from '@zilliz/milvus2-sdk-node';
 import { throwErrorFromSDK } from '../utils/Error';
 import { clientCache } from '../app';
+import { DatabaseObject } from '../types';
 
 export class DatabasesService {
   async createDatabase(clientId: string, data: CreateDatabaseRequest) {
@@ -19,20 +19,25 @@ export class DatabasesService {
   async listDatabase(
     clientId: string,
     data?: ListDatabasesRequest
-  ): Promise<ListDatabasesResponse> {
+  ): Promise<DatabaseObject[]> {
     const { milvusClient, database } = clientCache.get(clientId);
 
     const res = await milvusClient.listDatabases(data);
 
     // test if the user has permission to access the database, loop through all databases
     // and check if the user has permission to access the database
-    const availableDatabases: string[] = [];
+    const availableDatabases: DatabaseObject[] = [];
 
-    for (const db of res.db_names) {
+    for (let i = 0; i < res.db_names.length; i++) {
       try {
-        await milvusClient.use({ db_name: db });
+        await milvusClient.use({ db_name: res.db_names[i] });
         await milvusClient.listDatabases(data);
-        availableDatabases.push(db);
+        const collections = await milvusClient.showCollections();
+        availableDatabases.push({
+          name: res.db_names[i],
+          collections: collections.data.map(c => c.name),
+          createdTime: (res as any).created_timestamp[i] || -1,
+        });
       } catch (e) {
         // ignore
       }
@@ -42,7 +47,7 @@ export class DatabasesService {
     await milvusClient.use({ db_name: database });
 
     throwErrorFromSDK(res.status);
-    return { ...res, db_names: availableDatabases };
+    return availableDatabases;
   }
 
   async dropDatabase(clientId: string, data: DropDatabasesRequest) {
@@ -60,7 +65,7 @@ export class DatabasesService {
   }
 
   async hasDatabase(clientId: string, data: string) {
-    const { db_names } = await this.listDatabase(clientId);
-    return db_names.indexOf(data) !== -1;
+    const dbs = await this.listDatabase(clientId);
+    return dbs.map(d => d.name).indexOf(data) !== -1;
   }
 }

+ 6 - 0
server/src/types/collections.type.ts

@@ -95,3 +95,9 @@ export type CronJobObject = {
     collections: string[];
   };
 };
+
+export type DatabaseObject = {
+  name: string;
+  createdTime: number;
+  collections: string[];
+};