Collections.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. import { useCallback, useContext, useMemo, useState } from 'react';
  2. import { Link, useSearchParams } from 'react-router-dom';
  3. import { Theme } from '@mui/material';
  4. import { useTranslation } from 'react-i18next';
  5. import Highlighter from 'react-highlight-words';
  6. import { rootContext, authContext, dataContext } from '@/context';
  7. import { CollectionService } from '@/http';
  8. import { usePaginationHook } from '@/hooks';
  9. import AttuGrid from '@/components/grid/Grid';
  10. import CustomToolBar from '@/components/grid/ToolBar';
  11. import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
  12. import icons from '@/components/icons/Icons';
  13. import EmptyCard from '@/components/cards/EmptyCard';
  14. import StatusAction from '@/pages/databases/collections/StatusAction';
  15. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  16. import CreateCollectionDialog from '@/pages/dialogs/CreateCollectionDialog';
  17. import LoadCollectionDialog from '@/pages/dialogs/LoadCollectionDialog';
  18. import ReleaseCollectionDialog from '@/pages/dialogs/ReleaseCollectionDialog';
  19. import DropCollectionDialog from '@/pages/dialogs/DropCollectionDialog';
  20. import RenameCollectionDialog from '@/pages/dialogs/RenameCollectionDialog';
  21. import DuplicateCollectionDialog from '@/pages/dialogs/DuplicateCollectionDialog';
  22. import InsertDialog from '@/pages/dialogs/insert/Dialog';
  23. import ImportSampleDialog from '@/pages/dialogs/ImportSampleDialog';
  24. import { getLabelDisplayedRows } from '@/pages/search/Utils';
  25. import { LOADING_STATE } from '@/consts';
  26. import { formatNumber } from '@/utils';
  27. import Aliases from './Aliases';
  28. import { CollectionObject } from '@server/types';
  29. import { makeStyles } from '@mui/styles';
  30. const useStyles = makeStyles((theme: Theme) => ({
  31. root: {
  32. display: 'flex',
  33. flexDirection: 'column',
  34. height: `100%`,
  35. },
  36. emptyWrapper: {
  37. marginTop: theme.spacing(2),
  38. },
  39. icon: {
  40. fontSize: '14px',
  41. marginLeft: theme.spacing(0.5),
  42. },
  43. dialogContent: {
  44. lineHeight: '24px',
  45. fontSize: '16px',
  46. },
  47. link: {
  48. color: theme.palette.text.primary,
  49. display: 'inline-block',
  50. wordBreak: 'break-all',
  51. whiteSpace: 'nowrap',
  52. width: '150px',
  53. overflow: 'hidden',
  54. textOverflow: 'ellipsis',
  55. height: '20px',
  56. textDecoration: 'none',
  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. const { isManaged } = useContext(authContext);
  70. const { collections, database, loading, fetchCollections, fetchCollection } =
  71. useContext(dataContext);
  72. const [searchParams] = useSearchParams();
  73. const [search, setSearch] = useState<string>(
  74. (searchParams.get('search') as string) || ''
  75. );
  76. const [selectedCollections, setSelectedCollections] = useState<
  77. CollectionObject[]
  78. >([]);
  79. const { setDialog } = useContext(rootContext);
  80. const { t: collectionTrans } = useTranslation('collection');
  81. const { t: btnTrans } = useTranslation('btn');
  82. const { t: commonTrans } = useTranslation();
  83. const gridTrans = commonTrans('grid');
  84. const classes = useStyles();
  85. const QuestionIcon = icons.question;
  86. const SourceIcon = icons.source;
  87. const clearIndexCache = useCallback(async () => {
  88. await CollectionService.flush();
  89. }, []);
  90. const formatCollections = useMemo(() => {
  91. const filteredCollections = search
  92. ? collections.filter(collection =>
  93. collection.collection_name.includes(search)
  94. )
  95. : collections;
  96. return filteredCollections;
  97. }, [search, collections]);
  98. const {
  99. pageSize,
  100. handlePageSize,
  101. currentPage,
  102. handleCurrentPage,
  103. total,
  104. data: collectionList,
  105. handleGridSort,
  106. order,
  107. orderBy,
  108. } = usePaginationHook(formatCollections);
  109. const toolbarConfigs: ToolBarConfig[] = [
  110. {
  111. label: collectionTrans('create'),
  112. onClick: () => {
  113. setDialog({
  114. open: true,
  115. type: 'custom',
  116. params: {
  117. component: <CreateCollectionDialog />,
  118. },
  119. });
  120. },
  121. icon: 'add',
  122. },
  123. {
  124. type: 'button',
  125. btnVariant: 'text',
  126. btnColor: 'secondary',
  127. label: btnTrans('load'),
  128. onClick: () => {
  129. setDialog({
  130. open: true,
  131. type: 'custom',
  132. params: {
  133. component: (
  134. <LoadCollectionDialog
  135. collection={selectedCollections[0]}
  136. onLoad={async () => {
  137. setSelectedCollections([]);
  138. }}
  139. />
  140. ),
  141. },
  142. });
  143. },
  144. icon: 'load',
  145. disabled: data => {
  146. return (
  147. data.length !== 1 ||
  148. data[0].status !== LOADING_STATE.UNLOADED ||
  149. !data[0].schema.hasVectorIndex
  150. );
  151. },
  152. tooltip: btnTrans('loadColTooltip'),
  153. },
  154. {
  155. type: 'button',
  156. btnVariant: 'text',
  157. btnColor: 'secondary',
  158. label: btnTrans('release'),
  159. onClick: () => {
  160. setDialog({
  161. open: true,
  162. type: 'custom',
  163. params: {
  164. component: (
  165. <ReleaseCollectionDialog
  166. collection={selectedCollections[0]}
  167. onRelease={async () => {
  168. setSelectedCollections([]);
  169. }}
  170. />
  171. ),
  172. },
  173. });
  174. },
  175. icon: 'release',
  176. tooltip: btnTrans('releaseColTooltip'),
  177. disabled: data => {
  178. return data.length !== 1 || data[0].status !== LOADING_STATE.LOADED;
  179. },
  180. },
  181. {
  182. icon: 'uploadFile',
  183. type: 'button',
  184. btnVariant: 'text',
  185. btnColor: 'secondary',
  186. label: btnTrans('importFile'),
  187. tooltip: btnTrans('importFileTooltip'),
  188. onClick: () => {
  189. setDialog({
  190. open: true,
  191. type: 'custom',
  192. params: {
  193. component: (
  194. <InsertDialog
  195. collections={formatCollections}
  196. defaultSelectedCollection={
  197. selectedCollections.length === 1
  198. ? selectedCollections[0].collection_name
  199. : ''
  200. }
  201. // user can't select partition on collection page, so default value is ''
  202. defaultSelectedPartition={''}
  203. onInsert={async (collectionName: string) => {
  204. await fetchCollection(collectionName);
  205. setSelectedCollections([]);
  206. }}
  207. />
  208. ),
  209. },
  210. });
  211. },
  212. /**
  213. * insert validation:
  214. * 1. At least 1 available collection
  215. * 2. selected collections quantity shouldn't over 1
  216. */
  217. disabled: () =>
  218. collectionList.length === 0 || selectedCollections.length > 1,
  219. },
  220. {
  221. icon: 'edit',
  222. type: 'button',
  223. btnColor: 'secondary',
  224. btnVariant: 'text',
  225. onClick: () => {
  226. setDialog({
  227. open: true,
  228. type: 'custom',
  229. params: {
  230. component: (
  231. <RenameCollectionDialog
  232. cb={async () => {
  233. setSelectedCollections([]);
  234. }}
  235. collection={selectedCollections[0]}
  236. />
  237. ),
  238. },
  239. });
  240. },
  241. label: btnTrans('rename'),
  242. tooltip: btnTrans('renameTooltip'),
  243. disabled: data => data.length !== 1,
  244. },
  245. {
  246. icon: 'copy',
  247. type: 'button',
  248. btnVariant: 'text',
  249. onClick: () => {
  250. setDialog({
  251. open: true,
  252. type: 'custom',
  253. params: {
  254. component: (
  255. <DuplicateCollectionDialog
  256. cb={async () => {
  257. setSelectedCollections([]);
  258. }}
  259. collection={selectedCollections[0]}
  260. collections={collections}
  261. />
  262. ),
  263. },
  264. });
  265. },
  266. label: btnTrans('duplicate'),
  267. tooltip: btnTrans('duplicateTooltip'),
  268. disabled: data => data.length !== 1,
  269. },
  270. {
  271. icon: 'delete',
  272. type: 'button',
  273. btnVariant: 'text',
  274. onClick: () => {
  275. setDialog({
  276. open: true,
  277. type: 'custom',
  278. params: {
  279. component: (
  280. <DropCollectionDialog
  281. onDelete={async () => {
  282. setSelectedCollections([]);
  283. }}
  284. collections={selectedCollections}
  285. />
  286. ),
  287. },
  288. });
  289. },
  290. label: btnTrans('drop'),
  291. tooltip: btnTrans('deleteColTooltip'),
  292. disabledTooltip: btnTrans('deleteDisableTooltip'),
  293. disabled: data => data.length < 1,
  294. },
  295. {
  296. icon: 'refresh',
  297. type: 'button',
  298. btnVariant: 'text',
  299. onClick: () => {
  300. if (selectedCollections.length > 0) {
  301. for (const collection of selectedCollections) {
  302. fetchCollection(collection.collection_name);
  303. }
  304. } else {
  305. clearIndexCache();
  306. fetchCollections();
  307. }
  308. },
  309. disabled: () => {
  310. return loading;
  311. },
  312. label: btnTrans('refresh'),
  313. },
  314. {
  315. label: 'Search',
  316. icon: 'search',
  317. searchText: search,
  318. onSearch: (value: string) => {
  319. setSearch(value);
  320. },
  321. },
  322. ];
  323. const colDefinitions: ColDefinitionsType[] = [
  324. {
  325. id: 'collection_name',
  326. align: 'left',
  327. disablePadding: true,
  328. sortBy: 'collection_name',
  329. sortType: 'string',
  330. formatter({ collection_name }) {
  331. return (
  332. <Link
  333. to={`/databases/${database}/${collection_name}/overview`}
  334. className={classes.link}
  335. title={collection_name}
  336. >
  337. <Highlighter
  338. textToHighlight={collection_name}
  339. searchWords={[search]}
  340. highlightClassName={classes.highlight}
  341. />
  342. </Link>
  343. );
  344. },
  345. getStyle: () => {
  346. return { minWidth: '200px' };
  347. },
  348. label: collectionTrans('name'),
  349. },
  350. {
  351. id: 'status',
  352. align: 'left',
  353. disablePadding: false,
  354. sortBy: 'loadedPercentage',
  355. label: collectionTrans('status'),
  356. formatter(v) {
  357. return (
  358. <StatusAction
  359. status={v.status}
  360. percentage={v.loadedPercentage}
  361. collection={v}
  362. />
  363. );
  364. },
  365. getStyle: () => {
  366. return { minWidth: '130px' };
  367. },
  368. },
  369. {
  370. id: 'rowCount',
  371. align: 'left',
  372. disablePadding: false,
  373. sortBy: 'rowCount',
  374. label: (
  375. <span className="flex-center with-max-content">
  376. {collectionTrans('rowCount')}
  377. <CustomToolTip title={collectionTrans('entityCountInfo')}>
  378. <QuestionIcon classes={{ root: classes.icon }} />
  379. </CustomToolTip>
  380. </span>
  381. ),
  382. formatter(v) {
  383. return formatNumber(v.rowCount);
  384. },
  385. getStyle: () => {
  386. return { minWidth: '150px' };
  387. },
  388. },
  389. {
  390. id: 'description',
  391. align: 'left',
  392. disablePadding: false,
  393. label: (
  394. <span className="flex-center with-max-content">
  395. {collectionTrans('description')}
  396. </span>
  397. ),
  398. formatter(v) {
  399. return v.description || '--';
  400. },
  401. getStyle: () => {
  402. return { minWidth: '150px' };
  403. },
  404. },
  405. {
  406. id: 'createdTime',
  407. align: 'left',
  408. disablePadding: false,
  409. label: collectionTrans('createdTime'),
  410. formatter(data) {
  411. return new Date(data.createdTime).toLocaleString();
  412. },
  413. getStyle: () => {
  414. return { minWidth: '165px' };
  415. },
  416. },
  417. {
  418. id: 'import',
  419. align: 'center',
  420. disablePadding: false,
  421. label: '',
  422. showActionCell: true,
  423. isHoverAction: true,
  424. actionBarConfigs: [
  425. {
  426. onClick: (e: React.MouseEvent, row: CollectionObject) => {
  427. setDialog({
  428. open: true,
  429. type: 'custom',
  430. params: {
  431. component: (
  432. <ImportSampleDialog
  433. collection={row}
  434. cb={async (collectionName: string) => {
  435. await fetchCollection(collectionName);
  436. }}
  437. />
  438. ),
  439. },
  440. });
  441. },
  442. icon: 'source',
  443. label: 'Import',
  444. showIconMethod: 'renderFn',
  445. getLabel: () => 'Import sample data',
  446. renderIconFn: () => <SourceIcon />,
  447. },
  448. ],
  449. },
  450. ];
  451. if (!isManaged) {
  452. colDefinitions.splice(4, 0, {
  453. id: 'aliases',
  454. align: 'left',
  455. disablePadding: false,
  456. label: (
  457. <span className="flex-center with-max-content">
  458. {collectionTrans('alias')}
  459. <CustomToolTip title={collectionTrans('aliasInfo')}>
  460. <QuestionIcon classes={{ root: classes.icon }} />
  461. </CustomToolTip>
  462. </span>
  463. ),
  464. formatter(v) {
  465. return <Aliases aliases={v.aliases} collection={v} />;
  466. },
  467. getStyle: () => {
  468. return { minWidth: '120px' };
  469. },
  470. });
  471. }
  472. const handleSelectChange = (value: any) => {
  473. setSelectedCollections(value);
  474. };
  475. const handlePageChange = (e: any, page: number) => {
  476. handleCurrentPage(page);
  477. setSelectedCollections([]);
  478. };
  479. const CollectionIcon = icons.navCollection;
  480. return (
  481. <section className={classes.root}>
  482. {collections.length > 0 || loading ? (
  483. <AttuGrid
  484. toolbarConfigs={toolbarConfigs}
  485. colDefinitions={colDefinitions}
  486. rows={collectionList}
  487. rowCount={total}
  488. primaryKey="id"
  489. selected={selectedCollections}
  490. setSelected={handleSelectChange}
  491. page={currentPage}
  492. onPageChange={handlePageChange}
  493. rowsPerPage={pageSize}
  494. tableHeaderHeight={49}
  495. rowHeight={49}
  496. setRowsPerPage={handlePageSize}
  497. isLoading={loading}
  498. handleSort={handleGridSort}
  499. order={order}
  500. orderBy={orderBy}
  501. hideOnDisable={true}
  502. labelDisplayedRows={getLabelDisplayedRows(gridTrans.collections)}
  503. />
  504. ) : (
  505. <>
  506. <CustomToolBar toolbarConfigs={toolbarConfigs} />
  507. <EmptyCard
  508. wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
  509. icon={<CollectionIcon />}
  510. text={collectionTrans('noData')}
  511. />
  512. </>
  513. )}
  514. </section>
  515. );
  516. };
  517. export default Collections;