Collections.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
  2. import { Link, useSearchParams } from 'react-router-dom';
  3. import { makeStyles, Theme, Chip, Tooltip } 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 { Collection, MilvusService, DataService, MilvusIndex } from '@/http';
  13. import { useNavigationHook, usePaginationHook } from '@/hooks';
  14. import { ALL_ROUTER_TYPES } from '@/router/Types';
  15. import AttuGrid from '@/components/grid/Grid';
  16. import CustomToolBar from '@/components/grid/ToolBar';
  17. import { InsertDataParam } from './Types';
  18. import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
  19. import icons from '@/components/icons/Icons';
  20. import EmptyCard from '@/components/cards/EmptyCard';
  21. import StatusAction from '@/pages/collections/StatusAction';
  22. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  23. import CreateCollectionDialog from '../dialogs/CreateCollectionDialog';
  24. import LoadCollectionDialog from '../dialogs/LoadCollectionDialog';
  25. import ReleaseCollectionDialog from '../dialogs/ReleaseCollectionDialog';
  26. import DropCollectionDialog from '../dialogs/DropCollectionDialog';
  27. import RenameCollectionDialog from '../dialogs/RenameCollectionDialog';
  28. import DuplicateCollectionDialog from '../dialogs/DuplicateCollectionDailog';
  29. import EmptyDataDialog from '../dialogs/EmptyDataDialog';
  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. chip: {
  63. color: theme.palette.text.primary,
  64. marginRight: theme.spacing(0.5),
  65. background: `rgba(0, 0, 0, 0.04)`,
  66. },
  67. }));
  68. const Collections = () => {
  69. useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
  70. const { isManaged } = useContext(authContext);
  71. const { database } = useContext(dataContext);
  72. const [searchParams] = useSearchParams();
  73. const [search, setSearch] = useState<string>(
  74. (searchParams.get('search') as string) || ''
  75. );
  76. const [loading, setLoading] = useState<boolean>(false);
  77. const [selectedCollections, setSelectedCollections] = useState<Collection[]>(
  78. []
  79. );
  80. const { setDialog, openSnackBar } = useContext(rootContext);
  81. const { collections, setCollections } = useContext(webSocketContext);
  82. const { t: collectionTrans } = useTranslation('collection');
  83. const { t: btnTrans } = useTranslation('btn');
  84. const { t: successTrans } = useTranslation('success');
  85. const classes = useStyles();
  86. const InfoIcon = icons.info;
  87. const SourceIcon = icons.source;
  88. const consistencyTooltipsMap: Record<string, string> = {
  89. Strong: collectionTrans('consistencyStrongTooltip'),
  90. Bounded: collectionTrans('consistencyBoundedTooltip'),
  91. Session: collectionTrans('consistencySessionTooltip'),
  92. Eventually: collectionTrans('consistencyEventuallyTooltip'),
  93. };
  94. const checkCollectionStatus = useCallback((collections: Collection[]) => {
  95. const hasLoadingOrBuildingCollection = collections.some(
  96. v => checkLoading(v) || checkIndexBuilding(v)
  97. );
  98. // if some collection is building index or loading, start pulling data
  99. if (hasLoadingOrBuildingCollection) {
  100. MilvusService.triggerCron({
  101. name: WS_EVENTS.COLLECTION,
  102. type: WS_EVENTS_TYPE.START,
  103. });
  104. }
  105. }, []);
  106. const fetchData = useCallback(async () => {
  107. try {
  108. setLoading(true);
  109. const collections = await Collection.getCollections();
  110. setCollections(collections);
  111. checkCollectionStatus(collections);
  112. } finally {
  113. setLoading(false);
  114. }
  115. }, [setCollections, checkCollectionStatus]);
  116. const clearIndexCache = useCallback(async () => {
  117. await MilvusIndex.flush();
  118. }, []);
  119. useEffect(() => {
  120. fetchData();
  121. }, [fetchData, database]);
  122. const getVectorField = (collection: Collection) => {
  123. return collection.fieldWithIndexInfo!.find(
  124. d => d.fieldType === 'FloatVector' || d.fieldType === 'BinaryVector'
  125. );
  126. };
  127. const formatCollections = useMemo(() => {
  128. const filteredCollections = search
  129. ? collections.filter(collection =>
  130. collection.collectionName.includes(search)
  131. )
  132. : collections;
  133. const data = filteredCollections.map(v => {
  134. // const indexStatus = statusRes.find(item => item.collectionName === v.collectionName);
  135. Object.assign(v, {
  136. nameElement: (
  137. <Link
  138. to={`/collections/${v.collectionName}`}
  139. className={classes.link}
  140. title={v.collectionName}
  141. >
  142. <Highlighter
  143. textToHighlight={v.collectionName}
  144. searchWords={[search]}
  145. highlightClassName={classes.highlight}
  146. />
  147. </Link>
  148. ),
  149. features: (
  150. <>
  151. {v.autoID ? (
  152. <Tooltip
  153. title={collectionTrans('autoIDTooltip')}
  154. placement="top"
  155. arrow
  156. >
  157. <Chip
  158. className={classes.chip}
  159. label={collectionTrans('autoID')}
  160. size="small"
  161. />
  162. </Tooltip>
  163. ) : null}
  164. {v.enableDynamicField ? (
  165. <Tooltip
  166. title={collectionTrans('dynamicSchemaTooltip')}
  167. placement="top"
  168. arrow
  169. >
  170. <Chip
  171. className={classes.chip}
  172. label={collectionTrans('dynmaicSchema')}
  173. size="small"
  174. />
  175. </Tooltip>
  176. ) : null}
  177. <Tooltip
  178. title={consistencyTooltipsMap[v.consistency_level]}
  179. placement="top"
  180. arrow
  181. >
  182. <Chip
  183. className={classes.chip}
  184. label={v.consistency_level}
  185. size="small"
  186. />
  187. </Tooltip>
  188. </>
  189. ),
  190. statusElement: (
  191. <StatusAction
  192. status={v.status}
  193. onIndexCreate={fetchData}
  194. percentage={v.loadedPercentage}
  195. field={getVectorField(v)!}
  196. collectionName={v.collectionName}
  197. action={() => {
  198. setDialog({
  199. open: true,
  200. type: 'custom',
  201. params: {
  202. component:
  203. v.status === LOADING_STATE.UNLOADED ? (
  204. <LoadCollectionDialog
  205. collection={v.collectionName}
  206. onLoad={async () => {
  207. openSnackBar(
  208. successTrans('load', {
  209. name: collectionTrans('collection'),
  210. })
  211. );
  212. await fetchData();
  213. }}
  214. />
  215. ) : (
  216. <ReleaseCollectionDialog
  217. collection={v.collectionName}
  218. onRelease={async () => {
  219. openSnackBar(
  220. successTrans('release', {
  221. name: collectionTrans('collection'),
  222. })
  223. );
  224. await fetchData();
  225. }}
  226. />
  227. ),
  228. },
  229. });
  230. }}
  231. />
  232. ),
  233. _aliasElement: (
  234. <Aliases
  235. aliases={v.aliases}
  236. collectionName={v.collectionName}
  237. onCreate={fetchData}
  238. onDelete={fetchData}
  239. />
  240. ),
  241. });
  242. return v;
  243. });
  244. return data;
  245. }, [search, collections]);
  246. const {
  247. pageSize,
  248. handlePageSize,
  249. currentPage,
  250. handleCurrentPage,
  251. total,
  252. data: collectionList,
  253. handleGridSort,
  254. order,
  255. orderBy,
  256. } = usePaginationHook(formatCollections);
  257. const toolbarConfigs: ToolBarConfig[] = [
  258. {
  259. label: collectionTrans('create'),
  260. onClick: () => {
  261. setDialog({
  262. open: true,
  263. type: 'custom',
  264. params: {
  265. component: (
  266. <CreateCollectionDialog
  267. onCreate={async () => {
  268. openSnackBar(
  269. successTrans('create', {
  270. name: collectionTrans('collection'),
  271. })
  272. );
  273. await fetchData();
  274. }}
  275. />
  276. ),
  277. },
  278. });
  279. },
  280. icon: 'add',
  281. },
  282. {
  283. icon: 'uploadFile',
  284. type: 'button',
  285. btnVariant: 'text',
  286. btnColor: 'secondary',
  287. label: btnTrans('insert'),
  288. onClick: () => {
  289. setDialog({
  290. open: true,
  291. type: 'custom',
  292. params: {
  293. component: (
  294. <InsertDialog
  295. collections={formatCollections}
  296. defaultSelectedCollection={
  297. selectedCollections.length === 1
  298. ? selectedCollections[0].collectionName
  299. : ''
  300. }
  301. // user can't select partition on collection page, so default value is ''
  302. defaultSelectedPartition={''}
  303. handleInsert={async (
  304. collectionName: string,
  305. partitionName: string,
  306. fieldData: any[]
  307. ): Promise<{ result: boolean; msg: string }> => {
  308. const param: InsertDataParam = {
  309. partition_name: partitionName,
  310. fields_data: fieldData,
  311. };
  312. try {
  313. await DataService.insertData(collectionName, param);
  314. await DataService.flush(collectionName);
  315. // update collections
  316. fetchData();
  317. return { result: true, msg: '' };
  318. } catch (err: any) {
  319. const {
  320. response: {
  321. data: { message },
  322. },
  323. } = err;
  324. return { result: false, msg: message || '' };
  325. }
  326. }}
  327. />
  328. ),
  329. },
  330. });
  331. },
  332. /**
  333. * insert validation:
  334. * 1. At least 1 available collection
  335. * 2. selected collections quantity shouldn't over 1
  336. */
  337. disabled: () =>
  338. collectionList.length === 0 || selectedCollections.length > 1,
  339. },
  340. {
  341. icon: 'edit',
  342. type: 'button',
  343. btnColor: 'secondary',
  344. btnVariant: 'text',
  345. onClick: () => {
  346. setDialog({
  347. open: true,
  348. type: 'custom',
  349. params: {
  350. component: (
  351. <RenameCollectionDialog
  352. cb={async () => {
  353. openSnackBar(
  354. successTrans('rename', {
  355. name: collectionTrans('collection'),
  356. })
  357. );
  358. await fetchData();
  359. setSelectedCollections([]);
  360. }}
  361. collectionName={selectedCollections[0].collectionName}
  362. />
  363. ),
  364. },
  365. });
  366. },
  367. label: btnTrans('rename'),
  368. // tooltip: collectionTrans('deleteTooltip'),
  369. disabledTooltip: collectionTrans('renameTooltip'),
  370. disabled: data => data.length !== 1,
  371. },
  372. {
  373. icon: 'copy',
  374. type: 'button',
  375. btnVariant: 'text',
  376. onClick: () => {
  377. setDialog({
  378. open: true,
  379. type: 'custom',
  380. params: {
  381. component: (
  382. <DuplicateCollectionDialog
  383. cb={async () => {
  384. openSnackBar(
  385. successTrans('duplicate', {
  386. name: collectionTrans('collection'),
  387. })
  388. );
  389. setSelectedCollections([]);
  390. await fetchData();
  391. }}
  392. collectionName={selectedCollections[0].collectionName}
  393. collections={collections}
  394. />
  395. ),
  396. },
  397. });
  398. },
  399. label: btnTrans('duplicate'),
  400. // tooltip: collectionTrans('deleteTooltip'),
  401. disabledTooltip: collectionTrans('duplicateTooltip'),
  402. disabled: data => data.length !== 1,
  403. },
  404. {
  405. icon: 'deleteOutline',
  406. type: 'button',
  407. btnVariant: 'text',
  408. onClick: () => {
  409. setDialog({
  410. open: true,
  411. type: 'custom',
  412. params: {
  413. component: (
  414. <EmptyDataDialog
  415. cb={async () => {
  416. openSnackBar(
  417. successTrans('empty', {
  418. name: collectionTrans('collection'),
  419. })
  420. );
  421. setSelectedCollections([]);
  422. await fetchData();
  423. }}
  424. collectionName={selectedCollections[0].collectionName}
  425. />
  426. ),
  427. },
  428. });
  429. },
  430. label: btnTrans('empty'),
  431. disabledTooltip: collectionTrans('emptyDataDisableTooltip'),
  432. disabled: (data: any) => {
  433. if (data.length === 0 || data.length > 1) {
  434. return true;
  435. } else {
  436. return Number(data[0].loadedPercentage) !== 100;
  437. }
  438. },
  439. },
  440. {
  441. icon: 'delete',
  442. type: 'button',
  443. btnVariant: 'text',
  444. onClick: () => {
  445. setDialog({
  446. open: true,
  447. type: 'custom',
  448. params: {
  449. component: (
  450. <DropCollectionDialog
  451. onDelete={async () => {
  452. openSnackBar(
  453. successTrans('delete', {
  454. name: collectionTrans('collection'),
  455. })
  456. );
  457. await fetchData();
  458. setSelectedCollections([]);
  459. }}
  460. collections={selectedCollections}
  461. />
  462. ),
  463. },
  464. });
  465. },
  466. label: btnTrans('drop'),
  467. // tooltip: collectionTrans('deleteTooltip'),
  468. disabledTooltip: collectionTrans('deleteTooltip'),
  469. disabled: data => data.length !== 1,
  470. },
  471. {
  472. icon: 'refresh',
  473. type: 'button',
  474. btnVariant: 'text',
  475. onClick: () => {
  476. clearIndexCache();
  477. fetchData();
  478. },
  479. label: btnTrans('refresh'),
  480. },
  481. {
  482. label: 'Search',
  483. icon: 'search',
  484. searchText: search,
  485. onSearch: (value: string) => {
  486. setSearch(value);
  487. },
  488. },
  489. ];
  490. const colDefinitions: ColDefinitionsType[] = [
  491. {
  492. id: 'nameElement',
  493. align: 'left',
  494. disablePadding: true,
  495. sortBy: 'collectionName',
  496. label: collectionTrans('name'),
  497. },
  498. {
  499. id: 'statusElement',
  500. align: 'left',
  501. disablePadding: false,
  502. sortBy: 'status',
  503. label: collectionTrans('status'),
  504. },
  505. {
  506. id: 'features',
  507. align: 'left',
  508. disablePadding: true,
  509. sortBy: 'enableDynamicField',
  510. label: collectionTrans('features'),
  511. },
  512. {
  513. id: 'entityCount',
  514. align: 'left',
  515. disablePadding: false,
  516. label: (
  517. <span className="flex-center">
  518. {collectionTrans('rowCount')}
  519. <CustomToolTip title={collectionTrans('entityCountInfo')}>
  520. <InfoIcon classes={{ root: classes.icon }} />
  521. </CustomToolTip>
  522. </span>
  523. ),
  524. },
  525. {
  526. id: 'desc',
  527. align: 'left',
  528. disablePadding: false,
  529. label: collectionTrans('desc'),
  530. },
  531. {
  532. id: 'createdAt',
  533. align: 'left',
  534. disablePadding: false,
  535. label: collectionTrans('createdTime'),
  536. },
  537. {
  538. id: 'import',
  539. align: 'center',
  540. disablePadding: false,
  541. label: '',
  542. showActionCell: true,
  543. isHoverAction: true,
  544. actionBarConfigs: [
  545. {
  546. onClick: (e: React.MouseEvent, row: Collection) => {
  547. setDialog({
  548. open: true,
  549. type: 'custom',
  550. params: {
  551. component: (
  552. <ImportSampleDialog collection={row.collectionName} />
  553. ),
  554. },
  555. });
  556. },
  557. icon: 'source',
  558. label: 'Import',
  559. showIconMethod: 'renderFn',
  560. getLabel: () => 'Import sample data',
  561. renderIconFn: (row: Collection) => <SourceIcon />,
  562. },
  563. ],
  564. },
  565. ];
  566. if (!isManaged) {
  567. colDefinitions.splice(4, 0, {
  568. id: '_aliasElement',
  569. align: 'left',
  570. disablePadding: false,
  571. label: (
  572. <span className="flex-center">
  573. {collectionTrans('alias')}
  574. <CustomToolTip title={collectionTrans('aliasInfo')}>
  575. <InfoIcon classes={{ root: classes.icon }} />
  576. </CustomToolTip>
  577. </span>
  578. ),
  579. });
  580. }
  581. const handleSelectChange = (value: any) => {
  582. setSelectedCollections(value);
  583. };
  584. const handlePageChange = (e: any, page: number) => {
  585. handleCurrentPage(page);
  586. setSelectedCollections([]);
  587. };
  588. const CollectionIcon = icons.navCollection;
  589. return (
  590. <section className="page-wrapper">
  591. {collections.length > 0 || loading ? (
  592. <AttuGrid
  593. toolbarConfigs={toolbarConfigs}
  594. colDefinitions={colDefinitions}
  595. rows={collectionList}
  596. rowCount={total}
  597. primaryKey="collectionName"
  598. selected={selectedCollections}
  599. setSelected={handleSelectChange}
  600. page={currentPage}
  601. onPageChange={handlePageChange}
  602. rowsPerPage={pageSize}
  603. setRowsPerPage={handlePageSize}
  604. isLoading={loading}
  605. handleSort={handleGridSort}
  606. order={order}
  607. orderBy={orderBy}
  608. />
  609. ) : (
  610. <>
  611. <CustomToolBar toolbarConfigs={toolbarConfigs} />
  612. <EmptyCard
  613. wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
  614. icon={<CollectionIcon />}
  615. text={collectionTrans('noData')}
  616. />
  617. </>
  618. )}
  619. </section>
  620. );
  621. };
  622. export default Collections;