Collections.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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 AttuGrid 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.data_type === DataTypeEnum.VarChar
  181. ? {
  182. ...v,
  183. type_params: {
  184. max_length: v.max_length!,
  185. },
  186. }
  187. : v
  188. );
  189. await CollectionHttp.createCollection({
  190. ...data,
  191. consistency_level: data.consistency_level,
  192. });
  193. handleCloseDialog();
  194. openSnackBar(
  195. successTrans('create', { name: collectionTrans('collection') })
  196. );
  197. fetchData();
  198. };
  199. const handleRelease = async (data: CollectionView) => {
  200. const res = await CollectionHttp.releaseCollection(data._name);
  201. openSnackBar(
  202. successTrans('release', { name: collectionTrans('collection') })
  203. );
  204. fetchData();
  205. return res;
  206. };
  207. const handleLoad = async (data: CollectionView) => {
  208. const res = await CollectionHttp.loadCollection(data._name);
  209. openSnackBar(successTrans('load', { name: collectionTrans('collection') }));
  210. fetchData();
  211. return res;
  212. };
  213. const handleDelete = async () => {
  214. for (const item of selectedCollections) {
  215. await CollectionHttp.deleteCollection(item._name);
  216. }
  217. openSnackBar(
  218. successTrans('delete', { name: collectionTrans('collection') })
  219. );
  220. fetchData();
  221. handleCloseDialog();
  222. setSelectedCollections([]);
  223. };
  224. const handleSearch = (value: string) => {
  225. if (timer) {
  226. clearTimeout(timer);
  227. }
  228. setSearch(value);
  229. };
  230. const toolbarConfigs: ToolBarConfig[] = [
  231. {
  232. label: collectionTrans('create'),
  233. onClick: () => {
  234. setDialog({
  235. open: true,
  236. type: 'custom',
  237. params: {
  238. component: (
  239. <CreateCollection handleCreate={handleCreateCollection} />
  240. ),
  241. },
  242. });
  243. },
  244. icon: 'add',
  245. },
  246. {
  247. label: btnTrans('insert'),
  248. onClick: () => {
  249. handleInsertDialog(
  250. <InsertContainer
  251. collections={formatCollections}
  252. defaultSelectedCollection={
  253. selectedCollections.length === 1
  254. ? selectedCollections[0]._name
  255. : ''
  256. }
  257. // user can't select partition on collection page, so default value is ''
  258. defaultSelectedPartition={''}
  259. handleInsert={handleInsert}
  260. />
  261. );
  262. },
  263. /**
  264. * insert validation:
  265. * 1. At least 1 available collection
  266. * 2. selected collections quantity shouldn't over 1
  267. */
  268. disabled: () =>
  269. collectionList.length === 0 || selectedCollections.length > 1,
  270. btnVariant: 'outlined',
  271. },
  272. {
  273. type: 'iconBtn',
  274. onClick: () => {
  275. setDialog({
  276. open: true,
  277. type: 'custom',
  278. params: {
  279. component: (
  280. <DeleteTemplate
  281. label={btnTrans('delete')}
  282. title={dialogTrans('deleteTitle', {
  283. type: collectionTrans('collection'),
  284. })}
  285. text={collectionTrans('deleteWarning')}
  286. handleDelete={handleDelete}
  287. />
  288. ),
  289. },
  290. });
  291. },
  292. label: collectionTrans('delete'),
  293. icon: 'delete',
  294. // tooltip: collectionTrans('deleteTooltip'),
  295. disabledTooltip: collectionTrans('deleteTooltip'),
  296. disabled: data => data.length === 0,
  297. },
  298. // Todo: hide alias after we can get all alias from milvus.
  299. // {
  300. // type: 'iconBtn',
  301. // onClick: () => {
  302. // setDialog({
  303. // open: true,
  304. // type: 'custom',
  305. // params: {
  306. // component: (
  307. // <CreateAlias
  308. // collectionName={selectedCollections[0]._name}
  309. // cb={() => {
  310. // setSelectedCollections([]);
  311. // }}
  312. // />
  313. // ),
  314. // },
  315. // });
  316. // },
  317. // label: collectionTrans('alias'),
  318. // icon: 'alias',
  319. // disabledTooltip: collectionTrans('aliasTooltip'),
  320. // disabled: data => data.length !== 1,
  321. // },
  322. {
  323. label: 'Search',
  324. icon: 'search',
  325. searchText: search,
  326. onSearch: (value: string) => {
  327. handleSearch(value);
  328. },
  329. },
  330. ];
  331. const colDefinitions: ColDefinitionsType[] = [
  332. {
  333. id: 'nameElement',
  334. align: 'left',
  335. disablePadding: true,
  336. sortBy: '_name',
  337. label: collectionTrans('name'),
  338. },
  339. {
  340. id: 'statusElement',
  341. align: 'left',
  342. disablePadding: false,
  343. sortBy: '_status',
  344. label: collectionTrans('status'),
  345. },
  346. {
  347. id: 'consistency_level',
  348. align: 'left',
  349. disablePadding: false,
  350. label: collectionTrans('consistencyLevel'),
  351. },
  352. {
  353. id: '_rowCount',
  354. align: 'left',
  355. disablePadding: false,
  356. label: (
  357. <span className="flex-center">
  358. {collectionTrans('rowCount')}
  359. <CustomToolTip title={collectionTrans('tooltip')}>
  360. <InfoIcon classes={{ root: classes.icon }} />
  361. </CustomToolTip>
  362. </span>
  363. ),
  364. },
  365. {
  366. id: '_desc',
  367. align: 'left',
  368. disablePadding: false,
  369. label: collectionTrans('desc'),
  370. },
  371. {
  372. id: '_createdTime',
  373. align: 'left',
  374. disablePadding: false,
  375. label: collectionTrans('createdTime'),
  376. },
  377. {
  378. id: 'indexCreatingElement',
  379. align: 'left',
  380. disablePadding: false,
  381. label: '',
  382. },
  383. {
  384. id: 'action',
  385. align: 'center',
  386. disablePadding: false,
  387. label: '',
  388. showActionCell: true,
  389. isHoverAction: true,
  390. actionBarConfigs: [
  391. {
  392. onClick: (e: React.MouseEvent, row: CollectionView) => {
  393. const cb =
  394. row._status === LOADING_STATE.UNLOADED
  395. ? handleLoad
  396. : handleRelease;
  397. handleAction(row, cb);
  398. },
  399. icon: 'load',
  400. label: 'load',
  401. showIconMethod: 'renderFn',
  402. getLabel: (row: CollectionView) =>
  403. row._status === LOADING_STATE.UNLOADED ? 'load' : 'release',
  404. renderIconFn: (row: CollectionView) =>
  405. row._status === LOADING_STATE.UNLOADED ? (
  406. <LoadIcon />
  407. ) : (
  408. <ReleaseIcon />
  409. ),
  410. },
  411. ],
  412. },
  413. ];
  414. const handleSelectChange = (value: any) => {
  415. setSelectedCollections(value);
  416. };
  417. const handlePageChange = (e: any, page: number) => {
  418. handleCurrentPage(page);
  419. setSelectedCollections([]);
  420. };
  421. const CollectionIcon = icons.navCollection;
  422. return (
  423. <section className="page-wrapper">
  424. {collections.length > 0 || loading ? (
  425. <AttuGrid
  426. toolbarConfigs={toolbarConfigs}
  427. colDefinitions={colDefinitions}
  428. rows={collectionList}
  429. rowCount={total}
  430. primaryKey="_name"
  431. selected={selectedCollections}
  432. setSelected={handleSelectChange}
  433. page={currentPage}
  434. onChangePage={handlePageChange}
  435. rowsPerPage={pageSize}
  436. setRowsPerPage={handlePageSize}
  437. isLoading={loading}
  438. handleSort={handleGridSort}
  439. order={order}
  440. orderBy={orderBy}
  441. />
  442. ) : (
  443. <>
  444. <CustomToolBar toolbarConfigs={toolbarConfigs} />
  445. <EmptyCard
  446. wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
  447. icon={<CollectionIcon />}
  448. text={collectionTrans('noData')}
  449. />
  450. </>
  451. )}
  452. </section>
  453. );
  454. };
  455. export default Collections;