diff --git a/client/src/components/customTabList/CustomTabList.tsx b/client/src/components/customTabList/CustomTabList.tsx index 6f0d069e..bd1f3ca1 100644 --- a/client/src/components/customTabList/CustomTabList.tsx +++ b/client/src/components/customTabList/CustomTabList.tsx @@ -4,6 +4,10 @@ import { ITabListProps, ITabPanel } from './Types'; const useStyles = makeStyles((theme: Theme) => ({ wrapper: { + display: 'flex', + flexDirection: 'column', + flexBasis: 0, + flexGrow: 1, '& .MuiTab-wrapper': { textTransform: 'capitalize', fontWeight: 'bold', @@ -25,7 +29,7 @@ const useStyles = makeStyles((theme: Theme) => ({ flexBasis: 0, flexGrow: 1, marginTop: theme.spacing(2), - overflowY: 'auto', + overflow: 'hidden', }, })); @@ -65,10 +69,9 @@ const CustomTabList: FC = props => { }; return ( - <> +
= props => { {tab.component} ))} - +
); }; diff --git a/client/src/components/dialogs/LoadCollectionDialog.tsx b/client/src/components/dialogs/LoadCollectionDialog.tsx new file mode 100644 index 00000000..6b53dcce --- /dev/null +++ b/client/src/components/dialogs/LoadCollectionDialog.tsx @@ -0,0 +1,196 @@ +import { useEffect, useState, useContext, useMemo } from 'react'; +import { + Typography, + makeStyles, + Theme, + Switch, + FormControlLabel, +} from '@material-ui/core'; +import { useTranslation } from 'react-i18next'; +import { CollectionHttp } from '../../http/Collection'; +import { rootContext } from '../../context/Root'; +import { useFormValidation } from '../../hooks/Form'; +import { formatForm } from '../../utils/Form'; +import { parseJson, getNode } from '../../utils/Metric'; +import CustomInput from '../customInput/CustomInput'; +import { ITextfieldConfig } from '../customInput/Types'; +import DialogTemplate from '../customDialog/DialogTemplate'; +import { MilvusHttp } from '../../http/Milvus'; +import CustomToolTip from '../customToolTip/CustomToolTip'; +import { MILVUS_NODE_TYPE, MILVUS_DEPLOY_MODE } from '../../consts/Milvus'; +import icons from '../icons/Icons'; + +const useStyles = makeStyles((theme: Theme) => ({ + desc: { + marginBottom: theme.spacing(2), + maxWidth: 480, + }, + replicaDesc: { + marginBottom: theme.spacing(2), + maxWidth: 480, + }, + toggle: { + marginBottom: theme.spacing(2), + }, + icon: { + fontSize: '20px', + marginLeft: theme.spacing(0.5), + }, +})); + +const LoadCollectionDialog = (props: any) => { + const classes = useStyles(); + const { collection, onLoad } = props; + const { t: dialogTrans } = useTranslation('dialog'); + const { t: collectionTrans } = useTranslation('collection'); + const { t: btnTrans } = useTranslation('btn'); + const { t: warningTrans } = useTranslation('warning'); + const { handleCloseDialog } = useContext(rootContext); + const [form, setForm] = useState({ + replica: 0, + }); + const [enableRelica, setEnableRelica] = useState(false); + const [replicaToggle, setReplicaToggle] = useState(false); + + // check if it is cluster + useEffect(() => { + async function fetchData() { + const res = await MilvusHttp.getMetrics(); + const parsedJson = parseJson(res); + // get root cord + const rootCoords = getNode( + parsedJson.workingNodes, + MILVUS_NODE_TYPE.ROOTCOORD + ); + // get query nodes + const queryNodes = getNode( + parsedJson.workingNodes, + MILVUS_NODE_TYPE.QUERYNODE + ); + + const rootCoord = rootCoords[0]; + + // should we show replic toggle + const enableRelica = + rootCoord.infos.system_info.deploy_mode === + MILVUS_DEPLOY_MODE.DISTRIBUTED; + + // only show replica toggle in distributed mode && query node > 1 + if (enableRelica && queryNodes.length > 1) { + setForm({ + replica: queryNodes.length, + }); + setEnableRelica(enableRelica); + } + } + fetchData(); + }, []); + + // input state change + const handleInputChange = (value: number) => { + setForm({ replica: value }); + }; + // confirm action + const handleConfirm = async () => { + let params; + + if (enableRelica) { + params = { replica_number: Number(form.replica) }; + } + + // load collection request + await CollectionHttp.loadCollection(collection, params); + // close dialog + handleCloseDialog(); + // callback + onLoad && onLoad(); + }; + + // validator + const checkedForm = useMemo(() => { + return enableRelica ? [] : formatForm(form); + }, [form, enableRelica]); + + // validate + const { validation, checkIsValid, disabled } = useFormValidation(checkedForm); + + // input config + const inputConfig: ITextfieldConfig = { + label: collectionTrans('replicaNum'), + type: 'number', + key: 'replica', + onChange: handleInputChange, + variant: 'filled', + placeholder: collectionTrans('replicaNum'), + fullWidth: true, + validations: [], + required: enableRelica, + defaultValue: form.replica, + }; + + // if replica is enabled, add validation + if (enableRelica) { + inputConfig.validations?.push({ + rule: 'require', + errorText: warningTrans('required', { + name: collectionTrans('replicaNum'), + }), + }); + } + + // toggle enbale replica + const handleChange = () => { + setReplicaToggle(!replicaToggle); + }; + + const InfoIcon = icons.info; + + return ( + + + {collectionTrans('loadContent')} + + {enableRelica ? ( + <> + + } + label={ + + <> + {collectionTrans('enableRepica')} + + + + } + className={classes.toggle} + /> + + ) : null} + {replicaToggle ? ( + <> + + + ) : null} + + } + confirmLabel={btnTrans('load')} + handleConfirm={handleConfirm} + confirmDisabled={replicaToggle ? disabled : false} + /> + ); +}; + +export default LoadCollectionDialog; diff --git a/client/src/components/dialogs/ReleaseCollectionDialog.tsx b/client/src/components/dialogs/ReleaseCollectionDialog.tsx new file mode 100644 index 00000000..abb17ebe --- /dev/null +++ b/client/src/components/dialogs/ReleaseCollectionDialog.tsx @@ -0,0 +1,63 @@ +import { useContext, useState } from 'react'; +import { Typography, makeStyles, Theme } from '@material-ui/core'; +import { useTranslation } from 'react-i18next'; +import { CollectionHttp } from '../../http/Collection'; +import { rootContext } from '../../context/Root'; +import DialogTemplate from '../customDialog/DialogTemplate'; + +const useStyles = makeStyles((theme: Theme) => ({ + desc: { + margin: '8px 0 16px 0', + maxWidth: 480, + }, +})); + +const ReleaseCollectionDialog = (props: any) => { + const classes = useStyles(); + + const { collection, onRelease } = props; + const { t: dialogTrans } = useTranslation('dialog'); + const { t: btnTrans } = useTranslation('btn'); + const { handleCloseDialog } = useContext(rootContext); + const [disabled, setDisabled] = useState(false); + + // confirm action + const handleConfirm = async () => { + // disable confirm button + setDisabled(true); + try { + // release collection + await CollectionHttp.releaseCollection(collection); + // execute callback + onRelease && onRelease(); + // enable confirm button + setDisabled(false); + // close dialog + handleCloseDialog(); + } finally { + // enable confirm button + setDisabled(false); + } + }; + + return ( + + + {dialogTrans('releaseContent', { type: collection })} + + + } + confirmLabel={btnTrans('release')} + handleConfirm={handleConfirm} + confirmDisabled={disabled} + /> + ); +}; + +export default ReleaseCollectionDialog; diff --git a/client/src/consts/Milvus.tsx b/client/src/consts/Milvus.tsx index 16de5c10..2dd83ebb 100644 --- a/client/src/consts/Milvus.tsx +++ b/client/src/consts/Milvus.tsx @@ -216,4 +216,20 @@ export enum LOADING_STATE { export const DEFAULT_VECTORS = 100000; export const DEFAULT_SEFMENT_FILE_SIZE = 1024; -export const DEFAULT_MILVUS_PORT = 19530; \ No newline at end of file +export const DEFAULT_MILVUS_PORT = 19530; + +export enum MILVUS_NODE_TYPE { + ROOTCOORD = 'rootcoord', + QUERYCOORD = 'querycoord', + INDEXCOORD = 'indexcoord', + QUERYNODE = 'querynode', + INDEXNODE = 'indexnode', + DATACORD = 'datacord', + DATANODE = 'datanode', + PROXY = 'proxy', +} + +export enum MILVUS_DEPLOY_MODE { + DISTRIBUTED = 'DISTRIBUTED', + STANDALONE = 'STANDALONE', +} diff --git a/client/src/hooks/Dialog.tsx b/client/src/hooks/Dialog.tsx index f4b0fc83..5560cc28 100644 --- a/client/src/hooks/Dialog.tsx +++ b/client/src/hooks/Dialog.tsx @@ -1,76 +1,5 @@ import { ReactElement, useContext } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Typography } from '@material-ui/core'; import { rootContext } from '../context/Root'; -import { CollectionData, CollectionView } from '../pages/collections/Types'; -import { PartitionView } from '../pages/partitions/Types'; -import { LOADING_STATE } from '../consts/Milvus'; - -// handle release and load dialog -export interface LoadAndReleaseDialogHookProps { - type: 'partition' | 'collection'; -} - -export const useLoadAndReleaseDialogHook = ( - props: LoadAndReleaseDialogHookProps -) => { - const { type } = props; - const { setDialog } = useContext(rootContext); - const { t: dialogTrans } = useTranslation('dialog'); - const { t: btnTrans } = useTranslation('btn'); - const { t: partitionTrans } = useTranslation('partition'); - const { t: collectionTrans } = useTranslation('collection'); - - const name = - type === 'collection' - ? collectionTrans('collection') - : partitionTrans('partition'); - - const actionsMap = { - release: { - title: dialogTrans('releaseTitle', { type: name }), - component: ( - - {dialogTrans('releaseContent', { type: name })} - - ), - confirmLabel: btnTrans('release'), - }, - load: { - title: dialogTrans('loadTitle', { type: name }), - component: ( - - {dialogTrans('loadContent', { type: name })} - - ), - confirmLabel: btnTrans('load'), - }, - }; - - const handleAction = ( - data: PartitionView | CollectionView | CollectionData, - cb: (data: any) => Promise - ) => { - const actionType: 'release' | 'load' = - data._status === LOADING_STATE.UNLOADED ? 'load' : 'release'; - const { title, component, confirmLabel } = actionsMap[actionType]; - - setDialog({ - open: true, - type: 'notice', - params: { - title, - component, - confirmLabel, - confirm: () => cb(data), - }, - }); - }; - - return { - handleAction, - }; -}; export const useInsertDialogHook = () => { const { setDialog } = useContext(rootContext); diff --git a/client/src/http/Collection.ts b/client/src/http/Collection.ts index b91913ed..3166003d 100644 --- a/client/src/http/Collection.ts +++ b/client/src/http/Collection.ts @@ -4,6 +4,8 @@ import { DeleteEntitiesReq, InsertDataParam, LoadSampleParam, + LoadRelicaReq, + Replica, } from '../pages/collections/Types'; import { Field } from '../pages/schema/Types'; import { VectorSearchParam } from '../types/SearchTypes'; @@ -29,6 +31,7 @@ export class CollectionHttp extends BaseModel implements CollectionView { private schema!: { fields: Field[]; }; + private replicas!: Replica[]; static COLLECTIONS_URL = '/collections'; static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status'; @@ -67,9 +70,10 @@ export class CollectionHttp extends BaseModel implements CollectionView { return super.delete({ path: `${this.COLLECTIONS_URL}/${collectionName}` }); } - static loadCollection(collectionName: string) { + static loadCollection(collectionName: string, param?: LoadRelicaReq) { return super.update({ path: `${this.COLLECTIONS_URL}/${collectionName}/load`, + data: param, }); } @@ -195,4 +199,8 @@ export class CollectionHttp extends BaseModel implements CollectionView { ? dayjs(Number(this.createdTime)).format('YYYY-MM-DD HH:mm:ss') : ''; } + + get _replicas(): Replica[] { + return this.replicas; + } } diff --git a/client/src/i18n/en/collection.ts b/client/src/i18n/en/collection.ts index d7ba9320..af481f62 100644 --- a/client/src/i18n/en/collection.ts +++ b/client/src/i18n/en/collection.ts @@ -58,8 +58,11 @@ const collectionTrans = { // load dialog loadTitle: 'Load Collection', loadContent: - 'You are trying to load a collection with data. Only loaded collection can be searched.', + 'All search and query operations within Milvus are executed in memory, only loaded collection can be searched.', loadConfirmLabel: 'Load', + replicaNum: 'Replica number', + replicaDes: `With in-memory replicas, Milvus can load the same segment on multiple query nodes. The replica number can not exceed query node count.`, + enableRepica: `Enable in-memory replica`, // release dialog releaseTitle: 'Release Collection', diff --git a/client/src/pages/collections/Collection.tsx b/client/src/pages/collections/Collection.tsx index 6633704b..2d029c60 100644 --- a/client/src/pages/collections/Collection.tsx +++ b/client/src/pages/collections/Collection.tsx @@ -1,22 +1,40 @@ +import { useMemo } from 'react'; +import { useNavigate, useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { makeStyles, Theme } from '@material-ui/core'; import { useNavigationHook } from '../../hooks/Navigation'; import { ALL_ROUTER_TYPES } from '../../router/Types'; import CustomTabList from '../../components/customTabList/CustomTabList'; import { ITab } from '../../components/customTabList/Types'; import Partitions from '../partitions/Partitions'; -import { useNavigate, useLocation, useParams } from 'react-router-dom'; -import { useMemo } from 'react'; import { parseLocationSearch } from '../../utils/Format'; import Schema from '../schema/Schema'; import Query from '../query/Query'; import Preview from '../preview/Preview'; +import { TAB_EMUM } from './Types'; -enum TAB_EMUM { - 'schema', - 'partition', -} +const useStyles = makeStyles((theme: Theme) => ({ + wrapper: { + flexDirection: 'row', + gap: theme.spacing(4), + }, + 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 { collectionName = '' } = useParams<{ collectionName: string; }>(); @@ -60,9 +78,10 @@ const Collection = () => { ]; return ( -
+
diff --git a/client/src/pages/collections/Collections.tsx b/client/src/pages/collections/Collections.tsx index 4e4a511b..40181958 100644 --- a/client/src/pages/collections/Collections.tsx +++ b/client/src/pages/collections/Collections.tsx @@ -25,10 +25,9 @@ import { rootContext } from '../../context/Root'; import CreateCollection from './Create'; import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate'; import { CollectionHttp } from '../../http/Collection'; -import { - useInsertDialogHook, - useLoadAndReleaseDialogHook, -} from '../../hooks/Dialog'; +import { useInsertDialogHook } from '../../hooks/Dialog'; +import LoadCollectionDialog from '../../components/dialogs/LoadCollectionDialog'; +import ReleaseCollectionDialog from '../../components/dialogs/ReleaseCollectionDialog'; import Highlighter from 'react-highlight-words'; import InsertContainer from '../../components/insert/Container'; import ImportSample from '../../components/insert/ImportSample'; @@ -64,7 +63,6 @@ const useStyles = makeStyles((theme: Theme) => ({ const Collections = () => { useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS); - const { handleAction } = useLoadAndReleaseDialogHook({ type: 'collection' }); const { handleInsertDialog } = useInsertDialogHook(); const [searchParams] = useSearchParams(); const [search, setSearch] = useState( @@ -246,20 +244,16 @@ const Collections = () => { fetchData(); }; - const handleRelease = async (data: CollectionView) => { - const res = await CollectionHttp.releaseCollection(data._name); + const onRelease = async () => { openSnackBar( successTrans('release', { name: collectionTrans('collection') }) ); fetchData(); - return res; }; - const handleLoad = async (data: CollectionView) => { - const res = await CollectionHttp.loadCollection(data._name); + const onLoad = () => { openSnackBar(successTrans('load', { name: collectionTrans('collection') })); fetchData(); - return res; }; const handleDelete = async () => { @@ -448,11 +442,25 @@ const Collections = () => { actionBarConfigs: [ { onClick: (e: React.MouseEvent, row: CollectionView) => { - const cb = - row._status === LOADING_STATE.UNLOADED - ? handleLoad - : handleRelease; - handleAction(row, cb); + setDialog({ + open: true, + type: 'custom', + params: { + component: + row._status === LOADING_STATE.UNLOADED ? ( + + ) : ( + + ), + }, + }); + e.preventDefault(); }, icon: 'load', diff --git a/client/src/pages/collections/Types.ts b/client/src/pages/collections/Types.ts index ccf4c10b..e1ce3eb6 100644 --- a/client/src/pages/collections/Types.ts +++ b/client/src/pages/collections/Types.ts @@ -14,6 +14,22 @@ export interface CollectionData { _fields?: FieldData[]; _consistencyLevel?: string; _aliases: string[]; + _replicas: Replica[]; +} + +export interface Replica { + collectionID: string; + node_ids: string[]; + partition_ids: string[]; + replicaID: string; + shard_replicas: ShardReplica[]; +} + +export interface ShardReplica { + dm_channel_name: string; + leaderID: string; + leader_addr: string; + node_id: string[]; } export interface CollectionView extends CollectionData { @@ -129,6 +145,10 @@ export interface AliasesProps { onDelete?: Function; } +export interface LoadRelicaReq { + replica_number: number; +} + export enum TAB_EMUM { 'schema', 'partition', diff --git a/client/src/pages/overview/Overview.tsx b/client/src/pages/overview/Overview.tsx index 23f0f60d..11eb2b41 100644 --- a/client/src/pages/overview/Overview.tsx +++ b/client/src/pages/overview/Overview.tsx @@ -7,7 +7,6 @@ import { WS_EVENTS, WS_EVENTS_TYPE } from '../../consts/Http'; import { LOADING_STATE } from '../../consts/Milvus'; import { rootContext } from '../../context/Root'; import { webSokcetContext } from '../../context/WebSocket'; -import { useLoadAndReleaseDialogHook } from '../../hooks/Dialog'; import { useNavigationHook } from '../../hooks/Navigation'; import { CollectionHttp } from '../../http/Collection'; import { MilvusHttp } from '../../http/Milvus'; @@ -34,7 +33,6 @@ const useStyles = makeStyles((theme: Theme) => ({ const Overview = () => { useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW); - const { handleAction } = useLoadAndReleaseDialogHook({ type: 'collection' }); const classes = useStyles(); const theme = useTheme(); const { t: overviewTrans } = useTranslation('overview'); @@ -48,9 +46,7 @@ const Overview = () => { totalData: 0, }); const [loading, setLoading] = useState(false); - const { collections, setCollections } = useContext(webSokcetContext); - const { openSnackBar } = useContext(rootContext); const fetchData = useCallback(async () => { @@ -81,18 +77,11 @@ const Overview = () => { [collections] ); - const fetchRelease = async (data: CollectionData) => { - const name = data._name; - const res = await CollectionHttp.releaseCollection(name); + const onRelease = () => { openSnackBar( successTrans('release', { name: collectionTrans('collection') }) ); fetchData(); - return res; - }; - - const handleRelease = (data: CollectionData) => { - handleAction(data, fetchRelease); }; const statisticsData = useMemo(() => { @@ -134,7 +123,7 @@ const Overview = () => { ))} diff --git a/client/src/pages/overview/collectionCard/CollectionCard.tsx b/client/src/pages/overview/collectionCard/CollectionCard.tsx index 8ca3bff4..5acda227 100644 --- a/client/src/pages/overview/collectionCard/CollectionCard.tsx +++ b/client/src/pages/overview/collectionCard/CollectionCard.tsx @@ -1,5 +1,5 @@ import { makeStyles, Theme, Typography, Divider } from '@material-ui/core'; -import { FC } from 'react'; +import { FC, useContext } from 'react'; import CustomButton from '../../../components/customButton/CustomButton'; import icons from '../../../components/icons/Icons'; import Status from '../../../components/status/Status'; @@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next'; import CustomIconButton from '../../../components/customButton/CustomIconButton'; import { useNavigate, Link } from 'react-router-dom'; import { LOADING_STATE } from '../../../consts/Milvus'; +import ReleaseCollectionDialog from '../../../components/dialogs/ReleaseCollectionDialog'; +import { rootContext } from '../../../context/Root'; const useStyles = makeStyles((theme: Theme) => ({ wrapper: { @@ -36,9 +38,13 @@ const useStyles = makeStyles((theme: Theme) => ({ fontSize: '16px', }, content: { - display: 'flex', - alignItems: 'center', - marginBottom: theme.spacing(2), + margin: 0, + padding: 0, + '& > li': { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(0.5), + }, }, rowCount: { marginLeft: theme.spacing(1), @@ -70,15 +76,18 @@ const useStyles = makeStyles((theme: Theme) => ({ const CollectionCard: FC = ({ data, - handleRelease, + onRelease, wrapperClass = '', }) => { const classes = useStyles(); + const { setDialog } = useContext(rootContext); + const { _name: name, _status: status, _rowCount: rowCount, _loadedPercentage, + _replicas, } = data; const navigate = useNavigate(); // icons @@ -91,7 +100,15 @@ const CollectionCard: FC = ({ const { t: btnTrans } = useTranslation('btn'); const onReleaseClick = () => { - handleRelease(data); + setDialog({ + open: true, + type: 'custom', + params: { + component: ( + + ), + }, + }); }; const onVectorSearchClick = () => { @@ -111,16 +128,20 @@ const CollectionCard: FC = ({ {name} -
- {collectionTrans('rowCount')} - - - - {rowCount} -
+
    + {_replicas.length > 1 ? ( +
  • + {collectionTrans('replicaNum')}: + + {_replicas.length} + +
  • + ) : null} +
  • + {collectionTrans('rowCount')}: + {rowCount} +
  • +
void; + onRelease: () => void; wrapperClass?: string; } diff --git a/client/src/pages/partitions/Partitions.tsx b/client/src/pages/partitions/Partitions.tsx index 78a81df1..8b0bbb99 100644 --- a/client/src/pages/partitions/Partitions.tsx +++ b/client/src/pages/partitions/Partitions.tsx @@ -24,7 +24,7 @@ import { MilvusHttp } from '../../http/Milvus'; const useStyles = makeStyles((theme: Theme) => ({ wrapper: { - height: '100%', + height: `calc(100vh - 160px)`, }, icon: { fontSize: '20px', diff --git a/client/src/pages/schema/Schema.tsx b/client/src/pages/schema/Schema.tsx index 79039122..6831b7fe 100644 --- a/client/src/pages/schema/Schema.tsx +++ b/client/src/pages/schema/Schema.tsx @@ -13,7 +13,7 @@ import { IndexHttp } from '../../http/Index'; const useStyles = makeStyles((theme: Theme) => ({ wrapper: { - height: '100%', + height: `calc(100vh - 160px)`, }, icon: { fontSize: '20px', diff --git a/client/src/pages/system/SystemView.tsx b/client/src/pages/system/SystemView.tsx index d4fbb401..9d616c2b 100644 --- a/client/src/pages/system/SystemView.tsx +++ b/client/src/pages/system/SystemView.tsx @@ -11,6 +11,7 @@ import NodeListView from './NodeListView'; // import LineChartCard from './LineChartCard'; // import ProgressCard from './ProgressCard'; import DataCard from './DataCard'; +import { parseJson } from '../../utils/Metric'; const getStyles = makeStyles((theme: Theme) => ({ root: { @@ -62,49 +63,6 @@ const getStyles = makeStyles((theme: Theme) => ({ }, })); -const parseJson = (jsonData: any) => { - const nodes: any[] = []; - const childNodes: any[] = []; - - const system = { - // qps: Math.random() * 1000, - latency: Math.random() * 1000, - disk: 0, - diskUsage: 0, - memory: 0, - memoryUsage: 0, - }; - - const workingNodes = jsonData?.response?.nodes_info.filter( - (node: any) => node?.infos?.has_error !== true - ); - - workingNodes.forEach((node: any) => { - const type = node?.infos?.type; - if (node.connected) { - node.connected = node.connected.filter((v: any) => - workingNodes.find( - (item: any) => v.connected_identifier === item.identifier - ) - ); - } - // coordinator node - if (type?.toLowerCase().includes('coord')) { - nodes.push(node); - // other nodes - } else { - childNodes.push(node); - } - - const info = node.infos.hardware_infos; - system.memory += info.memory; - system.memoryUsage += info.memory_usage; - system.disk += info.disk; - system.diskUsage += info.disk_usage; - }); - return { nodes, childNodes, system }; -}; - /** * Todo: Milvus V2.0.0 Memory data is not ready for now, open it after Milvus ready. * @returns diff --git a/client/src/styles/common.css b/client/src/styles/common.css index dab2c039..c591b0ef 100644 --- a/client/src/styles/common.css +++ b/client/src/styles/common.css @@ -55,9 +55,4 @@ fieldset { .dialog-content { line-height: 24px; font-size: 16px; - text-transform: lowercase; -} - -.dialog-content::first-letter { - text-transform: uppercase; } diff --git a/client/src/styles/theme.ts b/client/src/styles/theme.ts index f4a938ef..05ab9d88 100644 --- a/client/src/styles/theme.ts +++ b/client/src/styles/theme.ts @@ -126,7 +126,7 @@ export const theme = createMuiTheme({ // style for element p body1: { fontSize: '14px', - lineHeight: '20px', + lineHeight: 1.5, }, // small caption body2: { diff --git a/client/src/utils/Metric.ts b/client/src/utils/Metric.ts new file mode 100644 index 00000000..134d19b9 --- /dev/null +++ b/client/src/utils/Metric.ts @@ -0,0 +1,48 @@ +import { MILVUS_NODE_TYPE } from '../consts/Milvus'; + +export const parseJson = (jsonData: any) => { + const nodes: any[] = []; + const childNodes: any[] = []; + + const system = { + // qps: Math.random() * 1000, + latency: Math.random() * 1000, + disk: 0, + diskUsage: 0, + memory: 0, + memoryUsage: 0, + }; + + const workingNodes = jsonData?.response?.nodes_info.filter( + (node: any) => node?.infos?.has_error !== true + ); + + workingNodes.forEach((node: any) => { + const type = node?.infos?.type; + if (node.connected) { + node.connected = node.connected.filter((v: any) => + workingNodes.find( + (item: any) => v.connected_identifier === item.identifier + ) + ); + } + // coordinator node + if (type?.toLowerCase().includes('coord')) { + nodes.push(node); + // other nodes + } else { + childNodes.push(node); + } + + const info = node.infos.hardware_infos; + system.memory += info.memory; + system.memoryUsage += info.memory_usage; + system.disk += info.disk; + system.diskUsage += info.disk_usage; + }); + return { nodes, childNodes, system, workingNodes }; +}; + +export const getNode = (nodes: any, type: MILVUS_NODE_TYPE) => { + return nodes.filter((n: any) => n.infos.type === type); +}; diff --git a/server/src/collections/collections.controller.ts b/server/src/collections/collections.controller.ts index b0211ba9..c7879e74 100644 --- a/server/src/collections/collections.controller.ts +++ b/server/src/collections/collections.controller.ts @@ -10,6 +10,7 @@ import { VectorSearchDto, QueryDto, } from './dto'; +import { LoadCollectionReq } from '@zilliz/milvus2-sdk-node/dist/milvus/types'; export class CollectionController { private collectionsService: CollectionsService; @@ -179,11 +180,14 @@ export class CollectionController { } async loadCollection(req: Request, res: Response, next: NextFunction) { - const name = req.params?.name; + const collection_name = req.params?.name; + const data = req.body; + const param: LoadCollectionReq = { collection_name }; + if (data.replica_number) { + param.replica_number = Number(data.replica_number); + } try { - const result = await this.collectionsService.loadCollection({ - collection_name: name, - }); + const result = await this.collectionsService.loadCollection(param); res.send(result); } catch (error) { next(error); @@ -305,4 +309,16 @@ export class CollectionController { next(error); } } + + async getReplicas(req: Request, res: Response, next: NextFunction) { + const collectionID = req.params?.collectionID; + try { + const result = await this.collectionsService.getReplicas({ + collectionID, + }); + res.send(result); + } catch (error) { + next(error); + } + } } diff --git a/server/src/collections/collections.service.ts b/server/src/collections/collections.service.ts index 68c55e3b..5239a001 100644 --- a/server/src/collections/collections.service.ts +++ b/server/src/collections/collections.service.ts @@ -20,7 +20,7 @@ import { ShowCollectionsReq, ShowCollectionsType, } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Collection'; -import { QueryDto, ImportSampleDto } from './dto'; +import { QueryDto, ImportSampleDto, GetReplicasDto } from './dto'; import { DeleteEntitiesReq } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Data'; export class CollectionsService { @@ -116,6 +116,11 @@ export class CollectionsService { return res; } + async getReplicas(data: GetReplicasDto) { + const res = await this.collectionManager.getReplicas(data); + return res; + } + async query( data: { collection_name: string; @@ -143,7 +148,7 @@ export class CollectionsService { * @returns {id:string, collection_name:string, schema:Field[], autoID:boolean, rowCount: string, consistency_level:string} */ async getAllCollections() { - const data = []; + const data: any = []; const res = await this.getCollections(); const loadedCollections = await this.getCollections({ type: ShowCollectionsType.Loaded, @@ -175,6 +180,12 @@ export class CollectionsService { ? '-1' : loadCollection.loadedPercentage; + const replicas: any = loadCollection + ? await this.getReplicas({ + collectionID: collectionInfo.collectionID, + }) + : []; + data.push({ aliases: collectionInfo.aliases, collection_name: name, @@ -187,11 +198,12 @@ export class CollectionsService { createdTime: parseInt(collectionInfo.created_utc_timestamp, 10), index_status: indexRes.state, consistency_level: collectionInfo.consistency_level, + replicas: replicas && replicas.replicas, }); } } // add default sort - Descending order - data.sort((a, b) => b.createdTime - a.createdTime); + data.sort((a: any, b: any) => b.createdTime - a.createdTime); return data; } diff --git a/server/src/collections/dto.ts b/server/src/collections/dto.ts index 7500cd82..9f0f2407 100644 --- a/server/src/collections/dto.ts +++ b/server/src/collections/dto.ts @@ -53,6 +53,11 @@ export class ImportSampleDto { readonly size: string; } +export class GetReplicasDto { + readonly collectionID: string; + readonly with_shard_nodes?: boolean; +} + export class VectorSearchDto { @IsOptional() partition_names?: string[]; diff --git a/server/src/crons/crons.controller.ts b/server/src/crons/crons.controller.ts index b1084049..38619c66 100644 --- a/server/src/crons/crons.controller.ts +++ b/server/src/crons/crons.controller.ts @@ -32,7 +32,7 @@ export class CronsController { async toggleCronJobByName(req: Request, res: Response, next: NextFunction) { const cronData = req.body; const milvusAddress = (req.headers[MILVUS_ADDRESS] as string) || ''; - console.log(cronData, milvusAddress); + // console.log(cronData, milvusAddress); try { const result = await this.cronsService.toggleCronJobByName({ ...cronData,