Collections.tsx 14 KB

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