import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; import { makeStyles, Theme, Chip, Tooltip } from '@material-ui/core'; import { useTranslation } from 'react-i18next'; import Highlighter from 'react-highlight-words'; import { rootContext, authContext, dataContext, webSocketContext, } from '@/context'; import { Collection, MilvusService, DataService, MilvusIndex } from '@/http'; import { useNavigationHook, usePaginationHook } from '@/hooks'; import { ALL_ROUTER_TYPES } from '@/router/Types'; import AttuGrid from '@/components/grid/Grid'; import CustomToolBar from '@/components/grid/ToolBar'; import { InsertDataParam } from './Types'; import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types'; import icons from '@/components/icons/Icons'; import EmptyCard from '@/components/cards/EmptyCard'; import StatusAction from '@/pages/collections/StatusAction'; import CustomToolTip from '@/components/customToolTip/CustomToolTip'; import CreateCollectionDialog from '../dialogs/CreateCollectionDialog'; import LoadCollectionDialog from '../dialogs/LoadCollectionDialog'; import ReleaseCollectionDialog from '../dialogs/ReleaseCollectionDialog'; import DropCollectionDialog from '../dialogs/DropCollectionDialog'; import RenameCollectionDialog from '../dialogs/RenameCollectionDialog'; import DuplicateCollectionDialog from '../dialogs/DuplicateCollectionDailog'; import EmptyDataDialog from '../dialogs/EmptyDataDialog'; import InsertDialog from '../dialogs/insert/Dialog'; import ImportSampleDialog from '../dialogs/ImportSampleDialog'; import { LOADING_STATE } from '@/consts'; import { WS_EVENTS, WS_EVENTS_TYPE } from '@server/utils/Const'; import { checkIndexBuilding, checkLoading } from '@/utils'; import Aliases from './Aliases'; const useStyles = makeStyles((theme: Theme) => ({ emptyWrapper: { marginTop: theme.spacing(2), }, icon: { fontSize: '20px', marginLeft: theme.spacing(0.5), }, dialogContent: { lineHeight: '24px', fontSize: '16px', }, link: { color: theme.palette.common.black, display: 'inline-block', wordBreak: 'break-all', whiteSpace: 'nowrap', width: '150px', overflow: 'hidden', textOverflow: 'ellipsis', height: '20px', }, highlight: { color: theme.palette.primary.main, backgroundColor: 'transparent', }, chip: { color: theme.palette.text.primary, marginRight: theme.spacing(0.5), background: `rgba(0, 0, 0, 0.04)`, }, })); const Collections = () => { useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS); const { isManaged } = useContext(authContext); const { database } = useContext(dataContext); const [searchParams] = useSearchParams(); const [search, setSearch] = useState( (searchParams.get('search') as string) || '' ); const [loading, setLoading] = useState(false); const [selectedCollections, setSelectedCollections] = useState( [] ); const { setDialog, openSnackBar } = useContext(rootContext); const { collections, setCollections } = useContext(webSocketContext); const { t: collectionTrans } = useTranslation('collection'); const { t: btnTrans } = useTranslation('btn'); const { t: successTrans } = useTranslation('success'); const classes = useStyles(); const InfoIcon = icons.info; const SourceIcon = icons.source; const consistencyTooltipsMap: Record = { Strong: collectionTrans('consistencyStrongTooltip'), Bounded: collectionTrans('consistencyBoundedTooltip'), Session: collectionTrans('consistencySessionTooltip'), Eventually: collectionTrans('consistencyEventuallyTooltip'), }; const checkCollectionStatus = useCallback((collections: Collection[]) => { const hasLoadingOrBuildingCollection = collections.some( v => checkLoading(v) || checkIndexBuilding(v) ); // if some collection is building index or loading, start pulling data if (hasLoadingOrBuildingCollection) { MilvusService.triggerCron({ name: WS_EVENTS.COLLECTION, type: WS_EVENTS_TYPE.START, }); } }, []); const fetchData = useCallback(async () => { try { setLoading(true); const collections = await Collection.getCollections(); setCollections(collections); checkCollectionStatus(collections); } finally { setLoading(false); } }, [setCollections, checkCollectionStatus]); const clearIndexCache = useCallback(async () => { await MilvusIndex.flush(); }, []); useEffect(() => { fetchData(); }, [fetchData, database]); const getVectorField = (collection: Collection) => { return collection.fieldWithIndexInfo!.find( d => d.fieldType === 'FloatVector' || d.fieldType === 'BinaryVector' ); }; const formatCollections = useMemo(() => { const filteredCollections = search ? collections.filter(collection => collection.collectionName.includes(search) ) : collections; const data = filteredCollections.map(v => { // const indexStatus = statusRes.find(item => item.collectionName === v.collectionName); Object.assign(v, { nameElement: ( ), features: ( <> {v.autoID ? ( ) : null} {v.enableDynamicField ? ( ) : null} ), statusElement: ( { setDialog({ open: true, type: 'custom', params: { component: v.status === LOADING_STATE.UNLOADED ? ( { openSnackBar( successTrans('load', { name: collectionTrans('collection'), }) ); await fetchData(); }} /> ) : ( { openSnackBar( successTrans('release', { name: collectionTrans('collection'), }) ); await fetchData(); }} /> ), }, }); }} /> ), _aliasElement: ( ), }); return v; }); return data; }, [search, collections]); const { pageSize, handlePageSize, currentPage, handleCurrentPage, total, data: collectionList, handleGridSort, order, orderBy, } = usePaginationHook(formatCollections); const toolbarConfigs: ToolBarConfig[] = [ { label: collectionTrans('create'), onClick: () => { setDialog({ open: true, type: 'custom', params: { component: ( { openSnackBar( successTrans('create', { name: collectionTrans('collection'), }) ); await fetchData(); }} /> ), }, }); }, icon: 'add', }, { icon: 'uploadFile', type: 'button', btnVariant: 'text', btnColor: 'secondary', label: btnTrans('insert'), onClick: () => { setDialog({ open: true, type: 'custom', params: { component: ( => { const param: InsertDataParam = { partition_name: partitionName, fields_data: fieldData, }; try { await DataService.insertData(collectionName, param); await DataService.flush(collectionName); // update collections fetchData(); return { result: true, msg: '' }; } catch (err: any) { const { response: { data: { message }, }, } = err; return { result: false, msg: message || '' }; } }} /> ), }, }); }, /** * insert validation: * 1. At least 1 available collection * 2. selected collections quantity shouldn't over 1 */ disabled: () => collectionList.length === 0 || selectedCollections.length > 1, }, { icon: 'edit', type: 'button', btnColor: 'secondary', btnVariant: 'text', onClick: () => { setDialog({ open: true, type: 'custom', params: { component: ( { openSnackBar( successTrans('rename', { name: collectionTrans('collection'), }) ); await fetchData(); setSelectedCollections([]); }} collectionName={selectedCollections[0].collectionName} /> ), }, }); }, label: btnTrans('rename'), // tooltip: collectionTrans('deleteTooltip'), disabledTooltip: collectionTrans('renameTooltip'), disabled: data => data.length !== 1, }, { icon: 'copy', type: 'button', btnVariant: 'text', onClick: () => { setDialog({ open: true, type: 'custom', params: { component: ( { openSnackBar( successTrans('duplicate', { name: collectionTrans('collection'), }) ); setSelectedCollections([]); await fetchData(); }} collectionName={selectedCollections[0].collectionName} collections={collections} /> ), }, }); }, label: btnTrans('duplicate'), // tooltip: collectionTrans('deleteTooltip'), disabledTooltip: collectionTrans('duplicateTooltip'), disabled: data => data.length !== 1, }, { icon: 'deleteOutline', type: 'button', btnVariant: 'text', onClick: () => { setDialog({ open: true, type: 'custom', params: { component: ( { openSnackBar( successTrans('empty', { name: collectionTrans('collection'), }) ); setSelectedCollections([]); await fetchData(); }} collectionName={selectedCollections[0].collectionName} /> ), }, }); }, label: btnTrans('empty'), disabledTooltip: collectionTrans('emptyDataDisableTooltip'), disabled: (data: any) => { if (data.length === 0 || data.length > 1) { return true; } else { return Number(data[0].loadedPercentage) !== 100; } }, }, { icon: 'delete', type: 'button', btnVariant: 'text', onClick: () => { setDialog({ open: true, type: 'custom', params: { component: ( { openSnackBar( successTrans('delete', { name: collectionTrans('collection'), }) ); await fetchData(); setSelectedCollections([]); }} collections={selectedCollections} /> ), }, }); }, label: btnTrans('drop'), // tooltip: collectionTrans('deleteTooltip'), disabledTooltip: collectionTrans('deleteTooltip'), disabled: data => data.length !== 1, }, { icon: 'refresh', type: 'button', btnVariant: 'text', onClick: () => { clearIndexCache(); fetchData(); }, label: btnTrans('refresh'), }, { label: 'Search', icon: 'search', searchText: search, onSearch: (value: string) => { setSearch(value); }, }, ]; const colDefinitions: ColDefinitionsType[] = [ { id: 'nameElement', align: 'left', disablePadding: true, sortBy: 'collectionName', label: collectionTrans('name'), }, { id: 'statusElement', align: 'left', disablePadding: false, sortBy: 'status', label: collectionTrans('status'), }, { id: 'features', align: 'left', disablePadding: true, sortBy: 'enableDynamicField', label: collectionTrans('features'), }, { id: 'entityCount', align: 'left', disablePadding: false, label: ( {collectionTrans('rowCount')} ), }, { id: 'desc', align: 'left', disablePadding: false, label: collectionTrans('desc'), }, { id: 'createdAt', align: 'left', disablePadding: false, label: collectionTrans('createdTime'), }, { id: 'import', align: 'center', disablePadding: false, label: '', showActionCell: true, isHoverAction: true, actionBarConfigs: [ { onClick: (e: React.MouseEvent, row: Collection) => { setDialog({ open: true, type: 'custom', params: { component: ( ), }, }); }, icon: 'source', label: 'Import', showIconMethod: 'renderFn', getLabel: () => 'Import sample data', renderIconFn: (row: Collection) => , }, ], }, ]; if (!isManaged) { colDefinitions.splice(4, 0, { id: '_aliasElement', align: 'left', disablePadding: false, label: ( {collectionTrans('alias')} ), }); } const handleSelectChange = (value: any) => { setSelectedCollections(value); }; const handlePageChange = (e: any, page: number) => { handleCurrentPage(page); setSelectedCollections([]); }; const CollectionIcon = icons.navCollection; return (
{collections.length > 0 || loading ? ( ) : ( <> } text={collectionTrans('noData')} /> )}
); }; export default Collections;