diff --git a/client/src/App.tsx b/client/src/App.tsx index ca3e462f..1fd87263 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,20 +6,23 @@ import { NavProvider } from './context/Navigation'; import { AuthProvider } from './context/Auth'; import { WebSocketProvider } from './context/WebSocket'; import { PrometheusProvider } from './context/Prometheus'; +import { DatabaseProvider } from './context/Database'; function App() { return ( - - - - - - - - - + + + + + + + + + + + ); diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx index bba4a3d3..19761b90 100644 --- a/client/src/components/layout/Header.tsx +++ b/client/src/components/layout/Header.tsx @@ -1,13 +1,15 @@ -import { FC, useContext } from 'react'; +import { FC, useContext, useEffect } from 'react'; import { makeStyles, Theme, createStyles, Typography } from '@material-ui/core'; -import { HeaderType } from './Types'; -import { navContext } from '../../context/Navigation'; -import icons from '../icons/Icons'; import { useNavigate } from 'react-router-dom'; -import { authContext } from '../../context/Auth'; +import { navContext } from '@/context/Navigation'; +import { databaseContext } from '@/context/Database'; +import { authContext } from '@/context/Auth'; import { useTranslation } from 'react-i18next'; -import { MilvusHttp } from '../../http/Milvus'; -import { MILVUS_ADDRESS } from '../../consts/Localstorage'; +import { MilvusHttp } from '@/http/Milvus'; +import { MILVUS_ADDRESS } from '@/consts/Localstorage'; +import CustomSelector from '@/components/customSelector/CustomSelector'; +import icons from '../icons/Icons'; +import { HeaderType } from './Types'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -22,7 +24,7 @@ const useStyles = makeStyles((theme: Theme) => justifyContent: 'space-between', alignItems: 'center', paddingTop: theme.spacing(3), - paddingLeft: theme.spacing(5), + paddingLeft: theme.spacing(4), flex: 1, }, navigation: { @@ -32,6 +34,7 @@ const useStyles = makeStyles((theme: Theme) => icon: { color: theme.palette.primary.main, cursor: 'pointer', + marginRight: theme.spacing(1), }, addressWrapper: { display: 'flex', @@ -54,14 +57,20 @@ const useStyles = makeStyles((theme: Theme) => }, }, }, + database: { + width: theme.spacing(16), + marginRight: theme.spacing(2), + }, }) ); const Header: FC = props => { const classes = useStyles(); const { navInfo } = useContext(navContext); + const { database, databases, setDatabase } = useContext(databaseContext); const { address, setAddress, setIsAuth } = useContext(authContext); const navigate = useNavigate(); + const { t: commonTrans } = useTranslation(); const statusTrans = commonTrans('status'); const BackIcon = icons.back; @@ -80,6 +89,12 @@ const Header: FC = props => { // navigate(0); }; + const useDatabase = async (database: string) => { + await MilvusHttp.useDatabase({ database }); + }; + + const dbOptions = databases.map(d => ({ value: d, label: d })); + return (
@@ -90,6 +105,20 @@ const Header: FC = props => { onClick={() => handleBack(navInfo.backPath)} /> )} + {navInfo.showDatabaseSelector ? ( + { + const database = e.target.value as string; + await useDatabase(database); + setDatabase(database); + }} + options={dbOptions} + variant="filled" + wrapperClass={classes.database} + /> + ) : null} {navInfo.navTitle} diff --git a/client/src/components/menu/NavMenu.tsx b/client/src/components/menu/NavMenu.tsx index d634f205..867d3ab7 100644 --- a/client/src/components/menu/NavMenu.tsx +++ b/client/src/components/menu/NavMenu.tsx @@ -132,7 +132,7 @@ const useStyles = makeStyles((theme: Theme) => }, }, expandIcon: { - left: '187px', + left: '160px', transform: 'rotateZ(180deg)', }, collapseIcon: { diff --git a/client/src/context/Database.tsx b/client/src/context/Database.tsx new file mode 100644 index 00000000..6b0e5c17 --- /dev/null +++ b/client/src/context/Database.tsx @@ -0,0 +1,36 @@ +import { createContext, useEffect, useState } from 'react'; +import { DatabaseHttp } from '@/http/Database'; +import { DatabaseContextType } from './Types'; + +export const databaseContext = createContext({ + database: 'default', + databases: ['default'], + setDatabase: () => {}, +}); + +const { Provider } = databaseContext; +export const DatabaseProvider = (props: { children: React.ReactNode }) => { + const [database, setDatabase] = useState('default'); + const [databases, setDatabases] = useState(['default']); + + const fetchDatabases = async () => { + const res = await DatabaseHttp.getDatabases(); + setDatabases(res.db_names); + }; + + useEffect(() => { + fetchDatabases(); + }, []); + + return ( + + {props.children} + + ); +}; diff --git a/client/src/context/Navigation.tsx b/client/src/context/Navigation.tsx index e82e3618..20b9699a 100644 --- a/client/src/context/Navigation.tsx +++ b/client/src/context/Navigation.tsx @@ -7,6 +7,7 @@ export const navContext = createContext({ navInfo: { navTitle: '', backPath: '', + showDatabaseSelector: false, }, setNavInfo: () => {}, }); @@ -15,9 +16,11 @@ const { Provider } = navContext; export const NavProvider = (props: { children: React.ReactNode }) => { const { t } = useTranslation('nav'); + const [navInfo, setNavInfo] = useState({ navTitle: t('overview'), backPath: '', + showDatabaseSelector: false, }); return {props.children}; diff --git a/client/src/context/Types.ts b/client/src/context/Types.ts index ef4668ea..574887eb 100644 --- a/client/src/context/Types.ts +++ b/client/src/context/Types.ts @@ -62,6 +62,12 @@ export type AuthContextType = { setIsAuth: Dispatch>; }; +export type DatabaseContextType = { + database: string; + databases: string[]; + setDatabase: Dispatch>; +}; + export type PrometheusContextType = { withPrometheus: boolean; setWithPrometheus: Dispatch>; diff --git a/client/src/context/WebSocket.tsx b/client/src/context/WebSocket.tsx index 5eab66bc..ccef613a 100644 --- a/client/src/context/WebSocket.tsx +++ b/client/src/context/WebSocket.tsx @@ -9,12 +9,12 @@ import { CollectionView } from '../pages/collections/Types'; import { checkIndexBuilding, checkLoading } from '../utils/Validation'; import { WebSocketType } from './Types'; -export const webSokcetContext = createContext({ +export const webSocketContext = createContext({ collections: [], setCollections: data => {}, }); -const { Provider } = webSokcetContext; +const { Provider } = webSocketContext; export const WebSocketProvider = (props: { children: React.ReactNode }) => { const [collections, setCollections] = useState([]); diff --git a/client/src/hooks/Navigation.ts b/client/src/hooks/Navigation.ts index f632fa71..92dfed2a 100644 --- a/client/src/hooks/Navigation.ts +++ b/client/src/hooks/Navigation.ts @@ -22,6 +22,7 @@ export const useNavigationHook = ( const navInfo: NavInfo = { navTitle: navTrans('overview'), backPath: '', + showDatabaseSelector: true, }; setNavInfo(navInfo); break; @@ -30,6 +31,7 @@ export const useNavigationHook = ( const navInfo: NavInfo = { navTitle: navTrans('database'), backPath: '', + showDatabaseSelector: false, }; setNavInfo(navInfo); break; @@ -38,6 +40,7 @@ export const useNavigationHook = ( const navInfo: NavInfo = { navTitle: navTrans('collection'), backPath: '', + showDatabaseSelector: true, }; setNavInfo(navInfo); break; @@ -46,6 +49,7 @@ export const useNavigationHook = ( const navInfo: NavInfo = { navTitle: collectionName, backPath: '/collections', + showDatabaseSelector: false, }; setNavInfo(navInfo); break; @@ -54,6 +58,7 @@ export const useNavigationHook = ( const navInfo: NavInfo = { navTitle: navTrans('search'), backPath: '', + showDatabaseSelector: true, }; setNavInfo(navInfo); break; @@ -62,6 +67,7 @@ export const useNavigationHook = ( const navInfo: NavInfo = { navTitle: navTrans('system'), backPath: '', + showDatabaseSelector: false, }; setNavInfo(navInfo); break; @@ -70,14 +76,7 @@ export const useNavigationHook = ( const navInfo: NavInfo = { navTitle: navTrans('user'), backPath: '', - }; - setNavInfo(navInfo); - break; - } - case ALL_ROUTER_TYPES.PLUGIN: { - const navInfo: NavInfo = { - navTitle: title, - backPath: '', + showDatabaseSelector: false, }; setNavInfo(navInfo); break; diff --git a/client/src/http/Milvus.ts b/client/src/http/Milvus.ts index 50667f8e..03fbfd72 100644 --- a/client/src/http/Milvus.ts +++ b/client/src/http/Milvus.ts @@ -8,7 +8,7 @@ export class MilvusHttp extends BaseModel { static FLUSH_URL = '/milvus/flush'; static METRICS_URL = '/milvus/metrics'; static VERSION_URL = '/milvus/version'; - + static USE_DB_URL = '/milvus/usedb'; static TIGGER_CRON_URL = '/crons'; constructor(props: {}) { @@ -58,4 +58,8 @@ export class MilvusHttp extends BaseModel { data, }); } + + static useDatabase(data: { database: string }) { + return super.create({ path: this.USE_DB_URL, data }); + } } diff --git a/client/src/pages/collections/Collections.tsx b/client/src/pages/collections/Collections.tsx index 263e7b91..1a7ac693 100644 --- a/client/src/pages/collections/Collections.tsx +++ b/client/src/pages/collections/Collections.tsx @@ -3,6 +3,7 @@ import { Link, useSearchParams } from 'react-router-dom'; import { makeStyles, Theme } from '@material-ui/core'; import { useTranslation } from 'react-i18next'; import { authContext } from '@/context/Auth'; +import { databaseContext } from '@/context/Database'; import { useNavigationHook } from '@/hooks/Navigation'; import { ALL_ROUTER_TYPES } from '@/router/Types'; import AttuGrid from '@/components/grid/Grid'; @@ -27,9 +28,9 @@ import Highlighter from 'react-highlight-words'; import InsertDialog from '../dialogs/insert/Dialog'; import ImportSampleDialog from '../dialogs/ImportSampleDialog'; import { MilvusHttp } from '@/http/Milvus'; -import { LOADING_STATE } from '../../consts/Milvus'; -import { webSokcetContext } from '@/context/WebSocket'; -import { WS_EVENTS, WS_EVENTS_TYPE } from '../../consts/Http'; +import { LOADING_STATE } from '@/consts/Milvus'; +import { webSocketContext } from '@/context/WebSocket'; +import { WS_EVENTS, WS_EVENTS_TYPE } from '@/consts/Http'; import { checkIndexBuilding, checkLoading } from '@/utils/Validation'; import Aliases from './Aliases'; @@ -59,6 +60,7 @@ const useStyles = makeStyles((theme: Theme) => ({ const Collections = () => { useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS); const { isManaged } = useContext(authContext); + const { database } = useContext(databaseContext); const [searchParams] = useSearchParams(); const [search, setSearch] = useState( @@ -70,7 +72,7 @@ const Collections = () => { >([]); const { setDialog, openSnackBar } = useContext(rootContext); - const { collections, setCollections } = useContext(webSokcetContext); + const { collections, setCollections } = useContext(webSocketContext); const { t: collectionTrans } = useTranslation('collection'); const { t: btnTrans } = useTranslation('btn'); const { t: successTrans } = useTranslation('success'); @@ -105,7 +107,7 @@ const Collections = () => { useEffect(() => { fetchData(); - }, [fetchData]); + }, [fetchData, database]); const formatCollections = useMemo(() => { const filteredCollections = search diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index aed4f90e..373fa24c 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -130,7 +130,7 @@ function Index() {
({ const Overview = () => { useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW); + const { database } = useContext(databaseContext); const classes = useStyles(); const theme = useTheme(); const { t: overviewTrans } = useTranslation('overview'); @@ -46,7 +47,7 @@ const Overview = () => { totalData: 0, }); const [loading, setLoading] = useState(false); - const { collections, setCollections } = useContext(webSokcetContext); + const { collections, setCollections } = useContext(webSocketContext); const { openSnackBar } = useContext(rootContext); const fetchData = useCallback(async () => { @@ -66,7 +67,7 @@ const Overview = () => { setStatistics(res); setCollections(collections); setLoading(false); - }, [setCollections]); + }, [setCollections, database]); useEffect(() => { fetchData(); diff --git a/client/src/pages/search/VectorSearch.tsx b/client/src/pages/search/VectorSearch.tsx index 2f5c1efe..c56d156c 100644 --- a/client/src/pages/search/VectorSearch.tsx +++ b/client/src/pages/search/VectorSearch.tsx @@ -1,24 +1,26 @@ +import { useCallback, useEffect, useMemo, useState, useContext } from 'react'; import { TextField, Typography, Button } from '@material-ui/core'; import { useTranslation } from 'react-i18next'; -import { useNavigationHook } from '@/hooks/Navigation'; +import { useLocation } from 'react-router-dom'; import { ALL_ROUTER_TYPES } from '@/router/Types'; +import { useNavigationHook } from '@/hooks/Navigation'; +import { useSearchResult } from '@/hooks/Result'; +import { usePaginationHook } from '@/hooks/Pagination'; +import { useTimeTravelHook } from '@/hooks/TimeTravel'; +import { databaseContext } from '@/context/Database'; import CustomSelector from '@/components/customSelector/CustomSelector'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import SearchParams from './SearchParams'; -import { DEFAULT_METRIC_VALUE_MAP } from '../../consts/Milvus'; -import { FieldOption, SearchResultView, VectorSearchParam } from './Types'; +import { ColDefinitionsType } from '@/components/grid/Types'; import AttuGrid from '@/components/grid/Grid'; import EmptyCard from '@/components/cards/EmptyCard'; import icons from '@/components/icons/Icons'; -import { usePaginationHook } from '@/hooks/Pagination'; import CustomButton from '@/components/customButton/CustomButton'; import SimpleMenu from '@/components/menu/SimpleMenu'; -import { TOP_K_OPTIONS } from './Constants'; import { Option } from '@/components/customSelector/Types'; +import Filter from '@/components/advancedSearch'; +import { Field } from '@/components/advancedSearch/Types'; +import { CustomDatePicker } from '@/components/customDatePicker/CustomDatePicker'; import { CollectionHttp } from '@/http/Collection'; -import { CollectionData, DataTypeEnum } from '../collections/Types'; import { IndexHttp } from '@/http/Index'; -import { getVectorSearchStyles } from './Styles'; import { parseValue } from '@/utils/Insert'; import { classifyFields, @@ -27,21 +29,21 @@ import { getNonVectorFieldsForFilter, getVectorFieldOptions, } from '@/utils/search'; -import { ColDefinitionsType } from '@/components/grid/Types'; -import Filter from '@/components/advancedSearch'; -import { Field } from '@/components/advancedSearch/Types'; -import { useLocation } from 'react-router-dom'; import { parseLocationSearch } from '@/utils/Format'; import { cloneObj, generateVector } from '@/utils/Common'; -import { CustomDatePicker } from '@/components/customDatePicker/CustomDatePicker'; -import { useTimeTravelHook } from '@/hooks/TimeTravel'; import { LOADING_STATE } from '@/consts/Milvus'; +import { DEFAULT_METRIC_VALUE_MAP } from '@/consts/Milvus'; import { getLabelDisplayedRows } from './Utils'; -import { useSearchResult } from '@/hooks/Result'; +import SearchParams from './SearchParams'; +import { getVectorSearchStyles } from './Styles'; +import { CollectionData, DataTypeEnum } from '../collections/Types'; +import { TOP_K_OPTIONS } from './Constants'; +import { FieldOption, SearchResultView, VectorSearchParam } from './Types'; const VectorSearch = () => { useNavigationHook(ALL_ROUTER_TYPES.SEARCH); const location = useLocation(); + const { database } = useContext(databaseContext); // i18n const { t: searchTrans } = useTranslation('search'); @@ -237,7 +239,7 @@ const VectorSearch = () => { const fetchCollections = useCallback(async () => { const collections = await CollectionHttp.getCollections(); setCollections(collections.filter(c => c._status === LOADING_STATE.LOADED)); - }, []); + }, [database]); const fetchFieldsWithIndex = useCallback( async (collectionName: string, collections: CollectionData[]) => { @@ -260,13 +262,18 @@ const VectorSearch = () => { const filterFields = getNonVectorFieldsForFilter(nonVectorFields); setFilterFields(filterFields); }, - [] + [collections] ); useEffect(() => { fetchCollections(); }, [fetchCollections]); + // clear selection if database is changed + useEffect(() => { + setSelectedCollection(''); + }, [database]); + // get field options with index when selected collection changed useEffect(() => { if (selectedCollection !== '') { diff --git a/client/src/pages/system/SystemView.tsx b/client/src/pages/system/SystemView.tsx index afd4cf40..c51732a2 100644 --- a/client/src/pages/system/SystemView.tsx +++ b/client/src/pages/system/SystemView.tsx @@ -16,7 +16,7 @@ import { parseJson } from '@/utils/Metric'; const getStyles = makeStyles((theme: Theme) => ({ root: { fontFamily: 'Roboto', - margin: '14px 40px', + margin: '12px 32px', position: 'relative', height: 'fit-content', display: 'flex', diff --git a/client/src/pages/user/Users.tsx b/client/src/pages/user/Users.tsx index 109d8422..2fb2222b 100644 --- a/client/src/pages/user/Users.tsx +++ b/client/src/pages/user/Users.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { useNavigate, useLocation, useParams } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { makeStyles, Theme } from '@material-ui/core'; import { useNavigationHook } from '@/hooks/Navigation'; diff --git a/client/src/router/Types.ts b/client/src/router/Types.ts index f3a413eb..5c4a0436 100644 --- a/client/src/router/Types.ts +++ b/client/src/router/Types.ts @@ -18,4 +18,5 @@ export enum ALL_ROUTER_TYPES { export type NavInfo = { navTitle: string; backPath: string; + showDatabaseSelector: boolean; }; diff --git a/server/src/milvus/dto.ts b/server/src/milvus/dto.ts index 6e943014..84b54bd3 100644 --- a/server/src/milvus/dto.ts +++ b/server/src/milvus/dto.ts @@ -10,6 +10,11 @@ export class CheckMilvusDto { readonly address: string; } +export class UseDatabaseDto { + @IsString() + readonly database: string; +} + export class FlushDto { @IsArray() @ArrayMinSize(1, { message: "At least need one collection name." }) diff --git a/server/src/milvus/milvus.controller.ts b/server/src/milvus/milvus.controller.ts index fae87c2d..16330f9c 100644 --- a/server/src/milvus/milvus.controller.ts +++ b/server/src/milvus/milvus.controller.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response, Router } from 'express'; import { dtoValidationMiddleware } from '../middlewares/validation'; import { MilvusService } from './milvus.service'; -import { ConnectMilvusDto, FlushDto } from './dto'; +import { ConnectMilvusDto, FlushDto, UseDatabaseDto } from './dto'; import { INSIGHT_CACHE } from '../utils/Const'; import packageJson from '../../package.json'; @@ -25,6 +25,11 @@ export class MilvusController { dtoValidationMiddleware(ConnectMilvusDto), this.connectMilvus.bind(this) ); + this.router.post( + '/usedb', + dtoValidationMiddleware(UseDatabaseDto), + this.useDatabase.bind(this) + ); this.router.post('/disconnect', this.closeConnection.bind(this)); this.router.get('/check', this.checkConnect.bind(this)); this.router.put( @@ -95,6 +100,17 @@ export class MilvusController { res.send(data); } + async useDatabase(req: Request, res: Response, next: NextFunction) { + const { database } = req.body; + + try { + const result = await this.milvusService.useDatabase(database); + res.send(result); + } catch (error) { + next(error); + } + } + closeConnection(req: Request, res: Response, next: NextFunction) { const result = this.milvusService.closeConnection(); res.send({ result }); diff --git a/server/src/milvus/milvus.service.ts b/server/src/milvus/milvus.service.ts index 2387860e..fef6b88e 100644 --- a/server/src/milvus/milvus.service.ts +++ b/server/src/milvus/milvus.service.ts @@ -101,4 +101,11 @@ export class MilvusService { const res = MilvusService.activeMilvusClient.closeConnection(); return res; } + + async useDatabase(db: string) { + const res = await MilvusService.activeMilvusClient.use({ + db_name: db, + }); + return res; + } }