diff --git a/client/src/pages/databases/collections/StatusAction.tsx b/client/src/pages/databases/collections/StatusAction.tsx index 3541bbef..0e9c6bbe 100644 --- a/client/src/pages/databases/collections/StatusAction.tsx +++ b/client/src/pages/databases/collections/StatusAction.tsx @@ -9,6 +9,15 @@ import Icons from '@/components/icons/Icons'; import CustomToolTip from '@/components/customToolTip/CustomToolTip'; import LoadCollectionDialog from '@/pages/dialogs/LoadCollectionDialog'; import ReleaseCollectionDialog from '@/pages/dialogs/ReleaseCollectionDialog'; +import LoadPartitionDialog from '@/pages/dialogs/LoadPartitionDialog'; +import ReleasePartitionDialog from '@/pages/dialogs/ReleasePartitionDialog'; +import type { PartitionData } from '@server/types'; + +// 扩展类型,添加 partition 和 onRefresh 属性 +type ExtendedStatusActionType = StatusActionType & { + partition?: PartitionData; + onRefresh?: () => void; +}; const StatusIndicator: FC<{ color: string; filled?: boolean }> = ({ color, @@ -27,7 +36,7 @@ const StatusIndicator: FC<{ color: string; filled?: boolean }> = ({ /> ); -const StatusAction: FC = props => { +const StatusAction: FC = props => { const { status, percentage = 0, @@ -36,6 +45,8 @@ const StatusAction: FC = props => { showLoadButton, createIndexElement, sx, + partition, + onRefresh, } = props; const theme = useTheme(); @@ -47,6 +58,8 @@ const StatusAction: FC = props => { paddingLeft: theme.spacing(0.5), }; + const isPartition = !!partition; + const LoadingIndicator = () => ( = props => { tooltip: collectionTrans('clickToLoad'), variant: 'outlined' as const, onClick: () => { - setDialog({ - open: true, - type: 'custom', - params: { - component: , - }, - }); + if (isPartition && partition) { + setDialog({ + open: true, + type: 'custom', + params: { + component: ( + + ), + }, + }); + } else { + setDialog({ + open: true, + type: 'custom', + params: { + component: , + }, + }); + } }, }, [LOADING_STATE.LOADED]: { @@ -141,13 +170,29 @@ const StatusAction: FC = props => { tooltip: collectionTrans('clickToRelease'), icon: , onClick: () => { - setDialog({ - open: true, - type: 'custom', - params: { - component: , - }, - }); + if (isPartition && partition) { + setDialog({ + open: true, + type: 'custom', + params: { + component: ( + + ), + }, + }); + } else { + setDialog({ + open: true, + type: 'custom', + params: { + component: , + }, + }); + } }, }, [LOADING_STATE.LOADING]: { @@ -174,6 +219,9 @@ const StatusAction: FC = props => { commonTrans, collection, setDialog, + isPartition, + partition, + onRefresh, ]); const renderStatusChip = () => { diff --git a/client/src/pages/databases/collections/partitions/Partitions.tsx b/client/src/pages/databases/collections/partitions/Partitions.tsx index 6cf00b5a..ca2be2b0 100644 --- a/client/src/pages/databases/collections/partitions/Partitions.tsx +++ b/client/src/pages/databases/collections/partitions/Partitions.tsx @@ -1,4 +1,4 @@ -import { Box } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import { useContext, useEffect, useState } from 'react'; import { useSearchParams, useParams } from 'react-router-dom'; import Highlighter from 'react-highlight-words'; @@ -15,6 +15,7 @@ import CreatePartitionDialog from '@/pages/dialogs/CreatePartitionDialog'; import DropPartitionDialog from '@/pages/dialogs/DropPartitionDialog'; import { formatNumber } from '@/utils'; import { getLabelDisplayedRows } from '@/pages/search/Utils'; +import StatusAction from '@/pages/databases/collections/StatusAction'; import type { PartitionData, ResStatus } from '@server/types'; const Partitions = () => { @@ -56,6 +57,19 @@ const Partitions = () => { fetchPartitions(collectionName); }, [collectionName]); + // 当有分区处于加载状态时,自动轮询刷新 + useEffect(() => { + const hasLoadingPartition = partitions.some(p => p.status === 'loading'); + + if (hasLoadingPartition) { + const interval = setInterval(() => { + fetchPartitions(collectionName); + }, 2000); // 每2秒刷新一次 + + return () => clearInterval(interval); + } + }, [partitions, collectionName]); + const list = search ? partitions.filter(p => p.name.includes(search)) : partitions; @@ -203,6 +217,51 @@ const Partitions = () => { }, label: t('name'), }, + { + id: 'status', + align: 'left', + disablePadding: false, + label: '状态', + formatter(partition) { + const partitionAsCollection = { + collection_name: collectionName, + schema: { + hasVectorIndex: true, + fields: [], + primaryField: {} as any, + vectorFields: [], + scalarFields: [], + dynamicFields: [], + functionFields: [], + enablePartitionKey: false, + }, + rowCount: 0, + createdTime: 0, + aliases: [], + description: '', + autoID: false, + id: String(partition.id), + loadedPercentage: partition.loadedPercentage || 0, + consistency_level: 'Bounded', + replicas: [], + status: partition.status || 'unloaded', + loaded: partition.status === 'loaded', + properties: [], + db_name: 'default', + } as any; + + return ( + fetchPartitions(collectionName)} + /> + ); + }, + }, { id: 'rowCount', align: 'left', diff --git a/client/src/pages/dialogs/LoadPartitionDialog.tsx b/client/src/pages/dialogs/LoadPartitionDialog.tsx new file mode 100644 index 00000000..cb9b7b57 --- /dev/null +++ b/client/src/pages/dialogs/LoadPartitionDialog.tsx @@ -0,0 +1,61 @@ +import { FC, useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { rootContext } from '@/context'; +import { PartitionService } from '@/http'; +import DialogTemplate from '@/components/customDialog/DialogTemplate'; +import type { PartitionData } from '@server/types'; +import { Typography } from '@mui/material'; + +interface LoadPartitionDialogProps { + partitions: PartitionData[]; + collectionName: string; + onRefresh?: () => void; +} + +const LoadPartitionDialog: FC = (props) => { + const { partitions, collectionName, onRefresh } = props; + const { handleCloseDialog, openSnackBar } = useContext(rootContext); + const { t: partitionTrans } = useTranslation('partition'); + const { t: dialogTrans } = useTranslation('dialog'); + const { t: btnTrans } = useTranslation('btn'); + const { t: successTrans } = useTranslation('success'); + const [loading, setLoading] = useState(false); + + const handleLoad = async () => { + try { + setLoading(true); + await PartitionService.loadPartition({ + collectionName, + partitionNames: partitions.map(p => p.name), + }); + + handleCloseDialog(); + openSnackBar(successTrans('load', { name: partitionTrans('partition') })); + + if (onRefresh) { + onRefresh(); + } + } catch (error) { + console.error('Failed to load partition:', error); + } finally { + setLoading(false); + } + }; + + return ( + + {partitionTrans('loadContent')} + + } + confirmLabel={btnTrans('load')} + handleConfirm={handleLoad} + confirmDisabled={loading} + /> + ); +}; + +export default LoadPartitionDialog; diff --git a/client/src/pages/dialogs/ReleasePartitionDialog.tsx b/client/src/pages/dialogs/ReleasePartitionDialog.tsx new file mode 100644 index 00000000..aa9f6e12 --- /dev/null +++ b/client/src/pages/dialogs/ReleasePartitionDialog.tsx @@ -0,0 +1,74 @@ +import { FC, useContext, useState } from 'react'; +import { Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { rootContext } from '@/context'; +import DialogTemplate from '@/components/customDialog/DialogTemplate'; +import { PartitionService } from '@/http'; +import type { PartitionData } from '@server/types'; + +interface ReleasePartitionDialogProps { + partitions: PartitionData[]; + collectionName: string; + onRefresh?: () => void; +} + +const ReleasePartitionDialog: FC = (props) => { + const { partitions, collectionName, onRefresh } = props; + const { handleCloseDialog, openSnackBar } = useContext(rootContext); + const { t: partitionTrans } = useTranslation('partition'); + const { t: dialogTrans } = useTranslation('dialog'); + const { t: btnTrans } = useTranslation('btn'); + const { t: successTrans } = useTranslation('success'); + const [disabled, setDisabled] = useState(false); + + const handleConfirm = async () => { + setDisabled(true); + try { + await PartitionService.releasePartition({ + collectionName, + partitionNames: partitions.map(p => p.name), + }); + + openSnackBar(successTrans('release', { name: partitionTrans('partition') })); + + if (onRefresh) { + setTimeout(() => onRefresh(), 1000); + } + + handleCloseDialog(); + } finally { + setDisabled(false); + } + }; + + const partitionName = partitions.map(p => p.name).join(', '); + + return ( + theme.palette.text.secondary, + }} + > + {dialogTrans('releaseContent', { + type: partitionName, + })} + + } + confirmLabel={btnTrans('release')} + handleConfirm={handleConfirm} + confirmDisabled={disabled} + /> + ); +}; + +export default ReleasePartitionDialog; diff --git a/server/src/partitions/partitions.service.ts b/server/src/partitions/partitions.service.ts index b8d77549..0c0a21aa 100644 --- a/server/src/partitions/partitions.service.ts +++ b/server/src/partitions/partitions.service.ts @@ -5,8 +5,10 @@ import { LoadPartitionsReq, ReleasePartitionsReq, ShowPartitionsReq, + GetLoadStateReq, + LoadState, } from '@zilliz/milvus2-sdk-node'; -import { findKeyValue } from '../utils/Helper'; +import { findKeyValue } from '../utils'; import { ROW_COUNT } from '../utils'; import { clientCache } from '../app'; import { PartitionData } from '../types'; @@ -24,11 +26,20 @@ export class PartitionsService { ...data, partition_name: name, }); + + const { status, loadedPercentage } = await this.getPartitionLoadState( + clientId, + data.collection_name, + name + ); + result.push({ name, id: res.partitionIDs[index], rowCount: findKeyValue(statistics.stats, ROW_COUNT), createdTime: res.created_utc_timestamps[index], + status, + loadedPercentage, }); } } @@ -79,4 +90,39 @@ export class PartitionsService { const res = await milvusClient.releasePartitions(data); return res; } + + async getLoadState(clientId: string, data: GetLoadStateReq) { + const { milvusClient } = clientCache.get(clientId); + const res = await milvusClient.getLoadState(data); + return res; + } + + // 提取的获取分区加载状态方法 + async getPartitionLoadState( + clientId: string, + collectionName: string, + partitionName: string + ): Promise<{ status: 'loaded' | 'loading' | 'unloaded'; loadedPercentage: number }> { + let status: 'loaded' | 'loading' | 'unloaded' = 'unloaded'; + let loadedPercentage = 0; + + try { + const loadStateRes = await this.getLoadState(clientId, { + collection_name: collectionName, + partition_names: [partitionName], + }); + + if (loadStateRes.state === LoadState.LoadStateLoaded) { + status = 'loaded'; + loadedPercentage = 100; + } else if (loadStateRes.state === LoadState.LoadStateLoading) { + status = 'loading'; + loadedPercentage = 50; + } + } catch (error) { + console.log('Failed to get partition load state:', error); + } + + return { status, loadedPercentage }; + } } diff --git a/server/src/types/partitions.type.ts b/server/src/types/partitions.type.ts index 202c54a9..7fe1da57 100644 --- a/server/src/types/partitions.type.ts +++ b/server/src/types/partitions.type.ts @@ -3,4 +3,6 @@ export type PartitionData = { id: number; rowCount: string | number; createdTime: string; + status?: 'loaded' | 'loading' | 'unloaded'; + loadedPercentage?: number; };