Collections.tsx 19 KB

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