2
0

Collections.tsx 14 KB

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