Collections.tsx 14 KB

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