Collections.tsx 12 KB

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