Collections.tsx 12 KB

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