Collections.tsx 13 KB

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