Collections.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
  2. import { Link, useSearchParams } from 'react-router-dom';
  3. import { makeStyles, Theme } from '@material-ui/core';
  4. import { useTranslation } from 'react-i18next';
  5. import Highlighter from 'react-highlight-words';
  6. import {
  7. rootContext,
  8. authContext,
  9. dataContext,
  10. webSocketContext,
  11. } from '@/context';
  12. import { useNavigationHook, usePaginationHook } from '@/hooks';
  13. import { ALL_ROUTER_TYPES } from '@/router/Types';
  14. import AttuGrid from '@/components/grid/Grid';
  15. import CustomToolBar from '@/components/grid/ToolBar';
  16. import { CollectionView, InsertDataParam } from './Types';
  17. import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
  18. import icons from '@/components/icons/Icons';
  19. import EmptyCard from '@/components/cards/EmptyCard';
  20. import Status from '@/components/status/Status';
  21. import { ChildrenStatusType } from '@/components/status/Types';
  22. import StatusIcon from '@/components/status/StatusIcon';
  23. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  24. import CreateCollectionDialog from '../dialogs/CreateCollectionDialog';
  25. import { CollectionHttp, MilvusHttp } from '@/http';
  26. import LoadCollectionDialog from '../dialogs/LoadCollectionDialog';
  27. import ReleaseCollectionDialog from '../dialogs/ReleaseCollectionDialog';
  28. import DropCollectionDialog from '../dialogs/DropCollectionDialog';
  29. import RenameCollectionDialog from '../dialogs/RenameCollectionDialog';
  30. import InsertDialog from '../dialogs/insert/Dialog';
  31. import ImportSampleDialog from '../dialogs/ImportSampleDialog';
  32. import { LOADING_STATE } from '@/consts';
  33. import { WS_EVENTS, WS_EVENTS_TYPE } from '@server/utils/Const';
  34. import { checkIndexBuilding, checkLoading } from '@/utils';
  35. import Aliases from './Aliases';
  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. display: 'inline-block',
  51. wordBreak: 'break-all',
  52. whiteSpace: 'nowrap',
  53. width: '150px',
  54. overflow: 'hidden',
  55. textOverflow: 'ellipsis',
  56. height: '20px',
  57. },
  58. highlight: {
  59. color: theme.palette.primary.main,
  60. backgroundColor: 'transparent',
  61. },
  62. }));
  63. const Collections = () => {
  64. useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
  65. const { isManaged } = useContext(authContext);
  66. const { database } = useContext(dataContext);
  67. const [searchParams] = useSearchParams();
  68. const [search, setSearch] = useState<string>(
  69. (searchParams.get('search') as string) || ''
  70. );
  71. const [loading, setLoading] = useState<boolean>(false);
  72. const [selectedCollections, setSelectedCollections] = useState<
  73. CollectionView[]
  74. >([]);
  75. const { setDialog, openSnackBar } = useContext(rootContext);
  76. const { collections, setCollections } = useContext(webSocketContext);
  77. const { t: collectionTrans } = useTranslation('collection');
  78. const { t: btnTrans } = useTranslation('btn');
  79. const { t: successTrans } = useTranslation('success');
  80. const classes = useStyles();
  81. const LoadIcon = icons.load;
  82. const ReleaseIcon = icons.release;
  83. const InfoIcon = icons.info;
  84. const SourceIcon = icons.source;
  85. const fetchData = useCallback(async () => {
  86. try {
  87. setLoading(true);
  88. const res = await CollectionHttp.getCollections();
  89. const hasLoadingOrBuildingCollection = res.some(
  90. v => checkLoading(v) || checkIndexBuilding(v)
  91. );
  92. // if some collection is building index or loading, start pulling data
  93. if (hasLoadingOrBuildingCollection) {
  94. MilvusHttp.triggerCron({
  95. name: WS_EVENTS.COLLECTION,
  96. type: WS_EVENTS_TYPE.START,
  97. });
  98. }
  99. setCollections(res);
  100. } finally {
  101. setLoading(false);
  102. }
  103. }, [setCollections]);
  104. useEffect(() => {
  105. fetchData();
  106. }, [fetchData, database]);
  107. const formatCollections = useMemo(() => {
  108. const filteredCollections = search
  109. ? collections.filter(collection => collection._name.includes(search))
  110. : collections;
  111. const data = filteredCollections.map(v => {
  112. // const indexStatus = statusRes.find(item => item._name === v._name);
  113. Object.assign(v, {
  114. nameElement: (
  115. <Link
  116. to={`/collections/${v._name}`}
  117. className={classes.link}
  118. title={v._name}
  119. >
  120. <Highlighter
  121. textToHighlight={v._name}
  122. searchWords={[search]}
  123. highlightClassName={classes.highlight}
  124. />
  125. </Link>
  126. ),
  127. statusElement: (
  128. <Status status={v._status} percentage={v._loadedPercentage} />
  129. ),
  130. indexCreatingElement: (
  131. <StatusIcon type={v._indexState || ChildrenStatusType.FINISH} />
  132. ),
  133. _aliasElement: (
  134. <Aliases
  135. aliases={v._aliases}
  136. collectionName={v._name}
  137. onCreate={fetchData}
  138. onDelete={fetchData}
  139. />
  140. ),
  141. });
  142. return v;
  143. });
  144. return data;
  145. }, [search, collections]);
  146. const {
  147. pageSize,
  148. handlePageSize,
  149. currentPage,
  150. handleCurrentPage,
  151. total,
  152. data: collectionList,
  153. handleGridSort,
  154. order,
  155. orderBy,
  156. } = usePaginationHook(formatCollections);
  157. const handleInsert = async (
  158. collectionName: string,
  159. partitionName: string,
  160. fieldData: any[]
  161. ): Promise<{ result: boolean; msg: string }> => {
  162. const param: InsertDataParam = {
  163. partition_name: partitionName,
  164. fields_data: fieldData,
  165. };
  166. try {
  167. await CollectionHttp.insertData(collectionName, param);
  168. await MilvusHttp.flush(collectionName);
  169. // update collections
  170. fetchData();
  171. return { result: true, msg: '' };
  172. } catch (err: any) {
  173. const {
  174. response: {
  175. data: { message },
  176. },
  177. } = err;
  178. return { result: false, msg: message || '' };
  179. }
  180. };
  181. const onCreate = () => {
  182. openSnackBar(
  183. successTrans('create', { name: collectionTrans('collection') })
  184. );
  185. fetchData();
  186. };
  187. const onRelease = async () => {
  188. openSnackBar(
  189. successTrans('release', { name: collectionTrans('collection') })
  190. );
  191. fetchData();
  192. };
  193. const onLoad = () => {
  194. openSnackBar(successTrans('load', { name: collectionTrans('collection') }));
  195. fetchData();
  196. };
  197. const onDelete = () => {
  198. openSnackBar(
  199. successTrans('delete', { name: collectionTrans('collection') })
  200. );
  201. fetchData();
  202. setSelectedCollections([]);
  203. };
  204. const onRename = () => {
  205. openSnackBar(
  206. successTrans('rename', { name: collectionTrans('collection') })
  207. );
  208. fetchData();
  209. setSelectedCollections([]);
  210. };
  211. const handleSearch = (value: string) => {
  212. setSearch(value);
  213. };
  214. const toolbarConfigs: ToolBarConfig[] = [
  215. {
  216. label: collectionTrans('create'),
  217. onClick: () => {
  218. setDialog({
  219. open: true,
  220. type: 'custom',
  221. params: {
  222. component: <CreateCollectionDialog onCreate={onCreate} />,
  223. },
  224. });
  225. },
  226. icon: 'add',
  227. },
  228. {
  229. icon: 'uploadFile',
  230. type: 'iconBtn',
  231. label: btnTrans('insert'),
  232. onClick: () => {
  233. setDialog({
  234. open: true,
  235. type: 'custom',
  236. params: {
  237. component: (
  238. <InsertDialog
  239. collections={formatCollections}
  240. defaultSelectedCollection={
  241. selectedCollections.length === 1
  242. ? selectedCollections[0]._name
  243. : ''
  244. }
  245. // user can't select partition on collection page, so default value is ''
  246. defaultSelectedPartition={''}
  247. handleInsert={handleInsert}
  248. />
  249. ),
  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. },
  261. {
  262. icon: 'edit',
  263. type: 'iconBtn',
  264. onClick: () => {
  265. setDialog({
  266. open: true,
  267. type: 'custom',
  268. params: {
  269. component: (
  270. <RenameCollectionDialog
  271. cb={onRename}
  272. collectionName={selectedCollections[0]._name}
  273. />
  274. ),
  275. },
  276. });
  277. },
  278. label: collectionTrans('rename'),
  279. // tooltip: collectionTrans('deleteTooltip'),
  280. disabledTooltip: collectionTrans('renameTooltip'),
  281. disabled: data => data.length !== 1,
  282. },
  283. {
  284. type: 'iconBtn',
  285. onClick: () => {
  286. setDialog({
  287. open: true,
  288. type: 'custom',
  289. params: {
  290. component: (
  291. <DropCollectionDialog
  292. onDelete={onDelete}
  293. collections={selectedCollections}
  294. />
  295. ),
  296. },
  297. });
  298. },
  299. label: collectionTrans('delete'),
  300. icon: 'delete',
  301. // tooltip: collectionTrans('deleteTooltip'),
  302. disabledTooltip: collectionTrans('deleteTooltip'),
  303. disabled: data => data.length === 0,
  304. },
  305. {
  306. type: 'iconBtn',
  307. onClick: () => {
  308. fetchData();
  309. },
  310. label: collectionTrans('delete'),
  311. icon: 'refresh',
  312. },
  313. {
  314. label: 'Search',
  315. icon: 'search',
  316. searchText: search,
  317. onSearch: (value: string) => {
  318. handleSearch(value);
  319. },
  320. },
  321. ];
  322. const colDefinitions: ColDefinitionsType[] = [
  323. {
  324. id: 'nameElement',
  325. align: 'left',
  326. disablePadding: true,
  327. sortBy: '_name',
  328. label: collectionTrans('name'),
  329. },
  330. {
  331. id: 'statusElement',
  332. align: 'left',
  333. disablePadding: false,
  334. sortBy: '_status',
  335. label: collectionTrans('status'),
  336. },
  337. {
  338. id: '_rowCount',
  339. align: 'left',
  340. disablePadding: false,
  341. label: (
  342. <span className="flex-center">
  343. {collectionTrans('rowCount')}
  344. <CustomToolTip title={collectionTrans('entityCountInfo')}>
  345. <InfoIcon classes={{ root: classes.icon }} />
  346. </CustomToolTip>
  347. </span>
  348. ),
  349. },
  350. {
  351. id: 'consistency_level',
  352. align: 'left',
  353. disablePadding: false,
  354. label: (
  355. <span className="flex-center">
  356. {collectionTrans('consistencyLevel')}
  357. <CustomToolTip title={collectionTrans('consistencyLevelInfo')}>
  358. <InfoIcon classes={{ root: classes.icon }} />
  359. </CustomToolTip>
  360. </span>
  361. ),
  362. },
  363. {
  364. id: '_desc',
  365. align: 'left',
  366. disablePadding: false,
  367. label: collectionTrans('desc'),
  368. },
  369. {
  370. id: '_createdTime',
  371. align: 'left',
  372. disablePadding: false,
  373. label: collectionTrans('createdTime'),
  374. },
  375. // {
  376. // id: 'indexCreatingElement',
  377. // align: 'left',
  378. // disablePadding: false,
  379. // label: '',
  380. // },
  381. {
  382. id: 'action',
  383. align: 'center',
  384. disablePadding: false,
  385. label: '',
  386. showActionCell: true,
  387. isHoverAction: true,
  388. actionBarConfigs: [
  389. {
  390. onClick: (e: React.MouseEvent, row: CollectionView) => {
  391. setDialog({
  392. open: true,
  393. type: 'custom',
  394. params: {
  395. component:
  396. row._status === LOADING_STATE.UNLOADED ? (
  397. <LoadCollectionDialog
  398. collection={row._name}
  399. onLoad={onLoad}
  400. />
  401. ) : (
  402. <ReleaseCollectionDialog
  403. collection={row._name}
  404. onRelease={onRelease}
  405. />
  406. ),
  407. },
  408. });
  409. e.preventDefault();
  410. },
  411. icon: 'load',
  412. label: 'load',
  413. showIconMethod: 'renderFn',
  414. getLabel: (row: CollectionView) =>
  415. row._status === LOADING_STATE.UNLOADED ? 'load' : 'release',
  416. renderIconFn: (row: CollectionView) =>
  417. row._status === LOADING_STATE.UNLOADED ? (
  418. <LoadIcon />
  419. ) : (
  420. <ReleaseIcon />
  421. ),
  422. },
  423. ],
  424. },
  425. {
  426. id: 'import',
  427. align: 'center',
  428. disablePadding: false,
  429. label: '',
  430. showActionCell: true,
  431. isHoverAction: true,
  432. actionBarConfigs: [
  433. {
  434. onClick: (e: React.MouseEvent, row: CollectionView) => {
  435. setDialog({
  436. open: true,
  437. type: 'custom',
  438. params: {
  439. component: <ImportSampleDialog collection={row._name} />,
  440. },
  441. });
  442. },
  443. icon: 'source',
  444. label: 'Import',
  445. showIconMethod: 'renderFn',
  446. getLabel: () => 'Import sample data',
  447. renderIconFn: (row: CollectionView) => <SourceIcon />,
  448. },
  449. ],
  450. },
  451. ];
  452. if (!isManaged) {
  453. colDefinitions.splice(3, 0, {
  454. id: '_aliasElement',
  455. align: 'left',
  456. disablePadding: false,
  457. label: (
  458. <span className="flex-center">
  459. {collectionTrans('alias')}
  460. <CustomToolTip title={collectionTrans('aliasInfo')}>
  461. <InfoIcon classes={{ root: classes.icon }} />
  462. </CustomToolTip>
  463. </span>
  464. ),
  465. });
  466. }
  467. const handleSelectChange = (value: any) => {
  468. setSelectedCollections(value);
  469. };
  470. const handlePageChange = (e: any, page: number) => {
  471. handleCurrentPage(page);
  472. setSelectedCollections([]);
  473. };
  474. const CollectionIcon = icons.navCollection;
  475. return (
  476. <section className="page-wrapper">
  477. {collections.length > 0 || loading ? (
  478. <AttuGrid
  479. toolbarConfigs={toolbarConfigs}
  480. colDefinitions={colDefinitions}
  481. rows={collectionList}
  482. rowCount={total}
  483. primaryKey="_name"
  484. selected={selectedCollections}
  485. setSelected={handleSelectChange}
  486. page={currentPage}
  487. onPageChange={handlePageChange}
  488. rowsPerPage={pageSize}
  489. setRowsPerPage={handlePageSize}
  490. isLoading={loading}
  491. handleSort={handleGridSort}
  492. order={order}
  493. orderBy={orderBy}
  494. />
  495. ) : (
  496. <>
  497. <CustomToolBar toolbarConfigs={toolbarConfigs} />
  498. <EmptyCard
  499. wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
  500. icon={<CollectionIcon />}
  501. text={collectionTrans('noData')}
  502. />
  503. </>
  504. )}
  505. </section>
  506. );
  507. };
  508. export default Collections;