Collections.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
  2. import { Link } from 'react-router-dom';
  3. import { useNavigationHook } from '../../hooks/Navigation';
  4. import { ALL_ROUTER_TYPES } from '../../router/Types';
  5. import MilvusGrid from '../../components/grid/Grid';
  6. import CustomToolBar from '../../components/grid/ToolBar';
  7. import {
  8. CollectionCreateParam,
  9. CollectionView,
  10. DataTypeEnum,
  11. InsertDataParam,
  12. } from './Types';
  13. import { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
  14. import { usePaginationHook } from '../../hooks/Pagination';
  15. import icons from '../../components/icons/Icons';
  16. import EmptyCard from '../../components/cards/EmptyCard';
  17. import Status from '../../components/status/Status';
  18. import { useTranslation } from 'react-i18next';
  19. import { ChildrenStatusType } from '../../components/status/Types';
  20. import { makeStyles, Theme } from '@material-ui/core';
  21. import StatusIcon from '../../components/status/StatusIcon';
  22. import CustomToolTip from '../../components/customToolTip/CustomToolTip';
  23. import { rootContext } from '../../context/Root';
  24. import CreateCollection from './Create';
  25. import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
  26. import { CollectionHttp } from '../../http/Collection';
  27. import {
  28. useInsertDialogHook,
  29. useLoadAndReleaseDialogHook,
  30. } from '../../hooks/Dialog';
  31. import Highlighter from 'react-highlight-words';
  32. import { parseLocationSearch } from '../../utils/Format';
  33. import InsertContainer from '../../components/insert/Container';
  34. import { MilvusHttp } from '../../http/Milvus';
  35. import { LOADING_STATE } from '../../consts/Milvus';
  36. import { webSokcetContext } from '../../context/WebSocket';
  37. import { WS_EVENTS, WS_EVENTS_TYPE } from '../../consts/Http';
  38. import { checkIndexBuilding, checkLoading } from '../../utils/Validation';
  39. import CreateAlias from './CreateAlias';
  40. const useStyles = makeStyles((theme: Theme) => ({
  41. emptyWrapper: {
  42. marginTop: theme.spacing(2),
  43. },
  44. icon: {
  45. fontSize: '20px',
  46. marginLeft: theme.spacing(0.5),
  47. },
  48. dialogContent: {
  49. lineHeight: '24px',
  50. fontSize: '16px',
  51. },
  52. link: {
  53. color: theme.palette.common.black,
  54. },
  55. highlight: {
  56. color: theme.palette.primary.main,
  57. backgroundColor: 'transparent',
  58. },
  59. }));
  60. let timer: NodeJS.Timeout | null = null;
  61. // get init search value from url
  62. const { urlSearch = '' } = parseLocationSearch(window.location.search);
  63. const Collections = () => {
  64. useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
  65. const { handleAction } = useLoadAndReleaseDialogHook({ type: 'collection' });
  66. const { handleInsertDialog } = useInsertDialogHook();
  67. const [search, setSearch] = useState<string>(urlSearch);
  68. const [loading, setLoading] = useState<boolean>(false);
  69. const [selectedCollections, setSelectedCollections] = useState<
  70. CollectionView[]
  71. >([]);
  72. const { setDialog, handleCloseDialog, openSnackBar } =
  73. useContext(rootContext);
  74. const { collections, setCollections } = useContext(webSokcetContext);
  75. const { t: collectionTrans } = useTranslation('collection');
  76. const { t: btnTrans } = useTranslation('btn');
  77. const { t: dialogTrans } = useTranslation('dialog');
  78. const { t: successTrans } = useTranslation('success');
  79. const classes = useStyles();
  80. const LoadIcon = icons.load;
  81. const ReleaseIcon = icons.release;
  82. const InfoIcon = icons.info;
  83. const searchedCollections = useMemo(
  84. () => collections.filter(collection => collection._name.includes(search)),
  85. [collections, search]
  86. );
  87. const formatCollections = useMemo(() => {
  88. const data = searchedCollections.map(v => {
  89. // const indexStatus = statusRes.find(item => item._name === v._name);
  90. Object.assign(v, {
  91. nameElement: (
  92. <Link to={`/collections/${v._name}`} className={classes.link}>
  93. <Highlighter
  94. textToHighlight={v._name}
  95. searchWords={[search]}
  96. highlightClassName={classes.highlight}
  97. />
  98. </Link>
  99. ),
  100. statusElement: (
  101. <Status status={v._status} percentage={v._loadedPercentage} />
  102. ),
  103. indexCreatingElement: (
  104. <StatusIcon type={v._indexState || ChildrenStatusType.FINISH} />
  105. ),
  106. });
  107. return v;
  108. });
  109. return data;
  110. }, [classes.highlight, classes.link, search, searchedCollections]);
  111. const {
  112. pageSize,
  113. handlePageSize,
  114. currentPage,
  115. handleCurrentPage,
  116. total,
  117. data: collectionList,
  118. handleGridSort,
  119. order,
  120. orderBy,
  121. } = usePaginationHook(formatCollections);
  122. const fetchData = useCallback(async () => {
  123. try {
  124. setLoading(true);
  125. const res = await CollectionHttp.getCollections();
  126. const hasLoadingOrBuildingCollection = res.some(
  127. v => checkLoading(v) || checkIndexBuilding(v)
  128. );
  129. // if some collection is building index or loading, start pulling data
  130. if (hasLoadingOrBuildingCollection) {
  131. MilvusHttp.triggerCron({
  132. name: WS_EVENTS.COLLECTION,
  133. type: WS_EVENTS_TYPE.START,
  134. });
  135. }
  136. setCollections(res);
  137. } finally {
  138. setLoading(false);
  139. }
  140. }, [setCollections]);
  141. useEffect(() => {
  142. fetchData();
  143. }, [fetchData]);
  144. const handleInsert = async (
  145. collectionName: string,
  146. partitionName: string,
  147. fieldData: any[]
  148. ): Promise<{ result: boolean; msg: string }> => {
  149. const param: InsertDataParam = {
  150. partition_names: [partitionName],
  151. fields_data: fieldData,
  152. };
  153. try {
  154. await CollectionHttp.insertData(collectionName, param);
  155. await MilvusHttp.flush(collectionName);
  156. // update collections
  157. fetchData();
  158. return { result: true, msg: '' };
  159. } catch (err: any) {
  160. const {
  161. response: {
  162. data: { message },
  163. },
  164. } = err;
  165. return { result: false, msg: message || '' };
  166. }
  167. };
  168. const handleCreateCollection = async (param: CollectionCreateParam) => {
  169. const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
  170. const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
  171. data.fields = data.fields.map(v =>
  172. vectorType.includes(v.data_type)
  173. ? {
  174. ...v,
  175. type_params: {
  176. // if data type is vector, dimension must exist.
  177. dim: v.dimension!,
  178. },
  179. }
  180. : v
  181. );
  182. await CollectionHttp.createCollection(data);
  183. handleCloseDialog();
  184. openSnackBar(
  185. successTrans('create', { name: collectionTrans('collection') })
  186. );
  187. fetchData();
  188. };
  189. const handleRelease = async (data: CollectionView) => {
  190. const res = await CollectionHttp.releaseCollection(data._name);
  191. openSnackBar(
  192. successTrans('release', { name: collectionTrans('collection') })
  193. );
  194. fetchData();
  195. return res;
  196. };
  197. const handleLoad = async (data: CollectionView) => {
  198. const res = await CollectionHttp.loadCollection(data._name);
  199. openSnackBar(successTrans('load', { name: collectionTrans('collection') }));
  200. fetchData();
  201. return res;
  202. };
  203. const handleDelete = async () => {
  204. for (const item of selectedCollections) {
  205. await CollectionHttp.deleteCollection(item._name);
  206. }
  207. openSnackBar(
  208. successTrans('delete', { name: collectionTrans('collection') })
  209. );
  210. fetchData();
  211. handleCloseDialog();
  212. setSelectedCollections([]);
  213. };
  214. const handleSearch = (value: string) => {
  215. if (timer) {
  216. clearTimeout(timer);
  217. }
  218. setSearch(value);
  219. };
  220. const toolbarConfigs: ToolBarConfig[] = [
  221. {
  222. label: collectionTrans('create'),
  223. onClick: () => {
  224. setDialog({
  225. open: true,
  226. type: 'custom',
  227. params: {
  228. component: (
  229. <CreateCollection handleCreate={handleCreateCollection} />
  230. ),
  231. },
  232. });
  233. },
  234. icon: 'add',
  235. },
  236. {
  237. label: btnTrans('insert'),
  238. onClick: () => {
  239. handleInsertDialog(
  240. <InsertContainer
  241. collections={formatCollections}
  242. defaultSelectedCollection={
  243. selectedCollections.length === 1
  244. ? selectedCollections[0]._name
  245. : ''
  246. }
  247. // user can't select partition on collection page, so default value is ''
  248. defaultSelectedPartition={''}
  249. handleInsert={handleInsert}
  250. />
  251. );
  252. },
  253. /**
  254. * insert validation:
  255. * 1. At least 1 available collection
  256. * 2. selected collections quantity shouldn't over 1
  257. */
  258. disabled: () =>
  259. collectionList.length === 0 || selectedCollections.length > 1,
  260. btnVariant: 'outlined',
  261. },
  262. {
  263. type: 'iconBtn',
  264. onClick: () => {
  265. setDialog({
  266. open: true,
  267. type: 'custom',
  268. params: {
  269. component: (
  270. <DeleteTemplate
  271. label={btnTrans('delete')}
  272. title={dialogTrans('deleteTitle', {
  273. type: collectionTrans('collection'),
  274. })}
  275. text={collectionTrans('deleteWarning')}
  276. handleDelete={handleDelete}
  277. />
  278. ),
  279. },
  280. });
  281. },
  282. label: collectionTrans('delete'),
  283. icon: 'delete',
  284. // tooltip: collectionTrans('deleteTooltip'),
  285. disabledTooltip: collectionTrans('deleteTooltip'),
  286. disabled: data => data.length === 0,
  287. },
  288. {
  289. type: 'iconBtn',
  290. onClick: () => {
  291. setDialog({
  292. open: true,
  293. type: 'custom',
  294. params: {
  295. component: (
  296. <CreateAlias
  297. collectionName={selectedCollections[0]._name}
  298. cb={() => {
  299. setSelectedCollections([]);
  300. }}
  301. />
  302. ),
  303. },
  304. });
  305. },
  306. label: collectionTrans('alias'),
  307. icon: 'alias',
  308. disabledTooltip: collectionTrans('aliasTooltip'),
  309. disabled: data => data.length !== 1,
  310. },
  311. {
  312. label: 'Search',
  313. icon: 'search',
  314. searchText: search,
  315. onSearch: (value: string) => {
  316. handleSearch(value);
  317. },
  318. },
  319. ];
  320. const colDefinitions: ColDefinitionsType[] = [
  321. {
  322. id: 'nameElement',
  323. align: 'left',
  324. disablePadding: true,
  325. sortBy: '_name',
  326. label: collectionTrans('name'),
  327. },
  328. {
  329. id: 'statusElement',
  330. align: 'left',
  331. disablePadding: false,
  332. sortBy: '_status',
  333. label: collectionTrans('status'),
  334. },
  335. {
  336. id: '_rowCount',
  337. align: 'left',
  338. disablePadding: false,
  339. label: (
  340. <span className="flex-center">
  341. {collectionTrans('rowCount')}
  342. <CustomToolTip title={collectionTrans('tooltip')}>
  343. <InfoIcon classes={{ root: classes.icon }} />
  344. </CustomToolTip>
  345. </span>
  346. ),
  347. },
  348. {
  349. id: '_desc',
  350. align: 'left',
  351. disablePadding: false,
  352. label: collectionTrans('desc'),
  353. },
  354. {
  355. id: '_createdTime',
  356. align: 'left',
  357. disablePadding: false,
  358. label: collectionTrans('createdTime'),
  359. },
  360. {
  361. id: 'indexCreatingElement',
  362. align: 'left',
  363. disablePadding: false,
  364. label: '',
  365. },
  366. {
  367. id: 'action',
  368. align: 'center',
  369. disablePadding: false,
  370. label: '',
  371. showActionCell: true,
  372. isHoverAction: true,
  373. actionBarConfigs: [
  374. {
  375. onClick: (e: React.MouseEvent, row: CollectionView) => {
  376. const cb =
  377. row._status === LOADING_STATE.UNLOADED
  378. ? handleLoad
  379. : handleRelease;
  380. handleAction(row, cb);
  381. },
  382. icon: 'load',
  383. label: 'load',
  384. showIconMethod: 'renderFn',
  385. getLabel: (row: CollectionView) =>
  386. row._status === LOADING_STATE.UNLOADED ? 'load' : 'release',
  387. renderIconFn: (row: CollectionView) =>
  388. row._status === LOADING_STATE.UNLOADED ? (
  389. <LoadIcon />
  390. ) : (
  391. <ReleaseIcon />
  392. ),
  393. },
  394. ],
  395. },
  396. ];
  397. const handleSelectChange = (value: any) => {
  398. setSelectedCollections(value);
  399. };
  400. const handlePageChange = (e: any, page: number) => {
  401. handleCurrentPage(page);
  402. setSelectedCollections([]);
  403. };
  404. const CollectionIcon = icons.navCollection;
  405. return (
  406. <section className="page-wrapper">
  407. {collections.length > 0 || loading ? (
  408. <MilvusGrid
  409. toolbarConfigs={toolbarConfigs}
  410. colDefinitions={colDefinitions}
  411. rows={collectionList}
  412. rowCount={total}
  413. primaryKey="_name"
  414. selected={selectedCollections}
  415. setSelected={handleSelectChange}
  416. page={currentPage}
  417. onChangePage={handlePageChange}
  418. rowsPerPage={pageSize}
  419. setRowsPerPage={handlePageSize}
  420. isLoading={loading}
  421. handleSort={handleGridSort}
  422. order={order}
  423. orderBy={orderBy}
  424. />
  425. ) : (
  426. <>
  427. <CustomToolBar toolbarConfigs={toolbarConfigs} />
  428. <EmptyCard
  429. wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
  430. icon={<CollectionIcon />}
  431. text={collectionTrans('noData')}
  432. />
  433. </>
  434. )}
  435. </section>
  436. );
  437. };
  438. export default Collections;