Schema.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. import { Typography, Tooltip, Box } from '@mui/material';
  2. import { useContext } from 'react';
  3. import { useParams, useNavigate } from 'react-router-dom';
  4. import AttuGrid from '@/components/grid/Grid';
  5. import { ColDefinitionsType } from '@/components/grid/Types';
  6. import { useTranslation } from 'react-i18next';
  7. import Icons from '@/components/icons/Icons';
  8. import { formatFieldType, formatNumber, findKeyValue } from '@/utils';
  9. import { dataContext, rootContext, systemContext } from '@/context';
  10. import IndexTypeElement from './IndexTypeElement';
  11. import { getLabelDisplayedRows } from '@/pages/search/Utils';
  12. import StatusAction from '@/pages/databases/collections/StatusAction';
  13. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  14. import {
  15. Wrapper,
  16. InfoWrapper,
  17. Card,
  18. InfoRow,
  19. InfoLabel,
  20. InfoValue,
  21. ActionWrapper,
  22. StyledChip,
  23. DataTypeChip,
  24. NameWrapper,
  25. ParamWrapper,
  26. GridWrapper,
  27. } from './StyledComponents';
  28. import LoadCollectionDialog from '@/pages/dialogs/LoadCollectionDialog';
  29. import RenameCollectionDialog from '@/pages/dialogs/RenameCollectionDialog';
  30. import EditMmapDialog from '@/pages/dialogs/EditMmapDialog';
  31. import DropCollectionDialog from '@/pages/dialogs/DropCollectionDialog';
  32. import CopyButton from '@/components/advancedSearch/CopyButton';
  33. import RefreshButton from '@/components/customButton/RefreshButton';
  34. import { CollectionService } from '@/http';
  35. import type { FieldObject } from '@server/types';
  36. const Overview = () => {
  37. const { fetchCollection, collections, loading, database } =
  38. useContext(dataContext);
  39. const { data } = useContext(systemContext);
  40. const { setDialog } = useContext(rootContext);
  41. const { collectionName = '' } = useParams<{ collectionName: string }>();
  42. const navigate = useNavigate();
  43. const { t: collectionTrans } = useTranslation('collection');
  44. const { t: indexTrans } = useTranslation('index');
  45. const { t: btnTrans } = useTranslation('btn');
  46. const { t: commonTrans } = useTranslation();
  47. const consistencyTooltipsMap: Record<string, string> = {
  48. Strong: collectionTrans('consistencyStrongTooltip'),
  49. Bounded: collectionTrans('consistencyBoundedTooltip'),
  50. Session: collectionTrans('consistencySessionTooltip'),
  51. Eventually: collectionTrans('consistencyEventuallyTooltip'),
  52. };
  53. // get collection
  54. const collection = collections.find(
  55. c => c.collection_name === collectionName
  56. );
  57. // fetch collection if not loaded
  58. if (collection && !collection.schema) {
  59. fetchCollection(collectionName);
  60. }
  61. // get fields
  62. const fields = collection?.schema?.fields || [];
  63. const colDefinitions: ColDefinitionsType[] = [
  64. {
  65. id: 'name',
  66. align: 'left',
  67. disablePadding: true,
  68. formatter(f: FieldObject) {
  69. return (
  70. <NameWrapper>
  71. <Typography
  72. variant="body1"
  73. sx={{
  74. color: f.name === '$meta' ? 'secondary.dark' : 'inherit',
  75. fontStyle: f.name === '$meta' ? 'italic' : 'inherit',
  76. }}
  77. component="div"
  78. >
  79. {f.name}
  80. </Typography>
  81. {f.name === '$meta' && (
  82. <StyledChip size="small" label="Dynamic field" />
  83. )}
  84. {f.is_primary_key && (
  85. <StyledChip
  86. size="small"
  87. label="PK"
  88. sx={{
  89. backgroundColor: theme => theme.palette.secondary.light,
  90. fontWeight: 600,
  91. fontSize: '12px',
  92. height: '20px',
  93. '& .MuiChip-label': {
  94. px: 1,
  95. },
  96. border: theme => `1px solid ${theme.palette.secondary.main}`,
  97. }}
  98. />
  99. )}
  100. {f.is_partition_key && (
  101. <StyledChip size="small" label="Partition key" />
  102. )}
  103. {findKeyValue(f.type_params, 'enable_match') && (
  104. <StyledChip size="small" label={collectionTrans('enableMatch')} />
  105. )}
  106. {findKeyValue(f.type_params, 'enable_analyzer') === 'true' && (
  107. <Tooltip
  108. title={findKeyValue(f.type_params, 'analyzer_params') as string}
  109. arrow
  110. >
  111. <StyledChip
  112. size="small"
  113. label={collectionTrans('analyzer')}
  114. onClick={() => {
  115. const textToCopy = findKeyValue(
  116. f.type_params,
  117. 'analyzer_params'
  118. );
  119. navigator.clipboard.writeText(textToCopy as string);
  120. }}
  121. />
  122. </Tooltip>
  123. )}
  124. {(findKeyValue(f.type_params, 'mmap.enabled') === 'true' ||
  125. isCollectionMmapEnabled) && (
  126. <Tooltip title={collectionTrans('mmapTooltip')} arrow>
  127. <StyledChip
  128. size="small"
  129. label={collectionTrans('mmapEnabled')}
  130. onClick={() => {
  131. setDialog({
  132. open: true,
  133. type: 'custom',
  134. params: {
  135. component: (
  136. <EditMmapDialog
  137. collection={collection!}
  138. cb={async () => {
  139. fetchCollection(collectionName);
  140. }}
  141. />
  142. ),
  143. },
  144. });
  145. }}
  146. />
  147. </Tooltip>
  148. )}
  149. {f.function && (
  150. <Tooltip title={JSON.stringify(f.function)} arrow>
  151. <StyledChip
  152. size="small"
  153. label={`
  154. ${
  155. f.is_function_output
  156. ? `<- ${f.function.type}(${f.function.input_field_names})`
  157. : ` ${collectionTrans('function')}: ${f.function.type}`
  158. }`}
  159. onClick={() => {
  160. const textToCopy = JSON.stringify(f.function);
  161. navigator.clipboard.writeText(textToCopy as string);
  162. }}
  163. />
  164. </Tooltip>
  165. )}
  166. </NameWrapper>
  167. );
  168. },
  169. label: collectionTrans('fieldName'),
  170. sortBy: 'name',
  171. },
  172. {
  173. id: 'data_type',
  174. align: 'left',
  175. disablePadding: false,
  176. formatter(f) {
  177. return (
  178. <Typography variant="body1" component="div">
  179. <DataTypeChip size="small" label={formatFieldType(f)} />
  180. </Typography>
  181. );
  182. },
  183. label: collectionTrans('fieldType'),
  184. },
  185. {
  186. id: 'nullable',
  187. align: 'left',
  188. disablePadding: false,
  189. label: collectionTrans('nullable'),
  190. formatter(f) {
  191. return (
  192. <Typography variant="body1">
  193. {f.nullable ? (
  194. <Icons.check sx={{ fontSize: '11px', ml: 0.5 }} />
  195. ) : (
  196. <Icons.cross2 sx={{ fontSize: '11px', ml: 0.5 }} />
  197. )}
  198. </Typography>
  199. );
  200. },
  201. },
  202. {
  203. id: 'default_value',
  204. align: 'left',
  205. disablePadding: false,
  206. label: collectionTrans('defaultValue'),
  207. formatter(f) {
  208. return (
  209. <Typography variant="body1">{f.default_value || '--'}</Typography>
  210. );
  211. },
  212. },
  213. {
  214. id: 'name',
  215. align: 'left',
  216. disablePadding: true,
  217. label: indexTrans('indexName'),
  218. formatter(f) {
  219. return <Typography variant="body1">{f.index?.index_name}</Typography>;
  220. },
  221. },
  222. {
  223. id: 'name',
  224. align: 'left',
  225. disablePadding: true,
  226. label: indexTrans('type'),
  227. notSort: true,
  228. formatter(f) {
  229. return (
  230. <Typography variant="body1" component="div">
  231. <IndexTypeElement
  232. field={f}
  233. collectionName={collectionName}
  234. cb={async () => {
  235. await fetchCollection(collectionName);
  236. }}
  237. />
  238. </Typography>
  239. );
  240. },
  241. },
  242. {
  243. id: 'name',
  244. align: 'left',
  245. disablePadding: false,
  246. label: indexTrans('param'),
  247. notSort: true,
  248. formatter(f) {
  249. return (
  250. <Typography variant="body1" component="div">
  251. {f.index ? (
  252. <ParamWrapper>
  253. {f.index.indexParameterPairs.length > 0 ? (
  254. f.index.indexParameterPairs.map((p: any) =>
  255. p.value ? (
  256. <div key={p.key + p.value}>
  257. <span className="param">
  258. <Typography variant="body1" className="key">
  259. {`${p.key}:`}
  260. </Typography>
  261. <Typography variant="body1" className="value">
  262. {p.value}
  263. </Typography>
  264. </span>
  265. </div>
  266. ) : (
  267. ''
  268. )
  269. )
  270. ) : (
  271. <>--</>
  272. )}
  273. </ParamWrapper>
  274. ) : (
  275. <>--</>
  276. )}
  277. </Typography>
  278. );
  279. },
  280. },
  281. {
  282. id: 'description',
  283. align: 'left',
  284. disablePadding: false,
  285. label: indexTrans('desc'),
  286. formatter(f) {
  287. return <Typography variant="body1">{f.description || '--'}</Typography>;
  288. },
  289. },
  290. ];
  291. // only show create index element when there is only one vector field
  292. let CreateIndexElement = null;
  293. if (
  294. collection &&
  295. collection.schema &&
  296. collection.schema.vectorFields.length === 1
  297. ) {
  298. CreateIndexElement = (
  299. <IndexTypeElement
  300. field={
  301. (collection.schema && collection.schema.vectorFields[0]) ||
  302. ({} as FieldObject)
  303. }
  304. collectionName={collectionName}
  305. cb={async () => {
  306. await fetchCollection(collectionName);
  307. }}
  308. />
  309. );
  310. }
  311. // enable modify replica if there are more than one query node
  312. const enableModifyReplica =
  313. data && data.queryNodes && data.queryNodes.length > 1;
  314. // if is autoID enabled
  315. const isAutoIDEnabled = collection?.schema?.fields.some(
  316. f => f.autoID === true
  317. );
  318. // check if collection is mmap enabled
  319. const isCollectionMmapEnabled = collection?.properties?.some((p: any) => {
  320. return p.key === 'mmap.enabled' && p.value === 'true';
  321. });
  322. // get loading state label
  323. return (
  324. <Wrapper>
  325. {collection && (
  326. <InfoWrapper>
  327. <Card>
  328. <InfoRow>
  329. <InfoLabel>{collectionTrans('name')}</InfoLabel>
  330. <InfoValue>
  331. <Tooltip title={collection.collection_name} arrow>
  332. <Typography
  333. variant="body1"
  334. sx={{ fontWeight: 500 }}
  335. className="truncate"
  336. >
  337. {collection.collection_name}
  338. </Typography>
  339. </Tooltip>
  340. <ActionWrapper>
  341. <RefreshButton
  342. sx={{
  343. '& svg': {
  344. width: 14,
  345. height: 14,
  346. },
  347. }}
  348. onClick={async () => {
  349. setDialog({
  350. open: true,
  351. type: 'custom',
  352. params: {
  353. component: (
  354. <RenameCollectionDialog
  355. collection={collection}
  356. cb={async newName => {
  357. await fetchCollection(newName);
  358. navigate(
  359. `/databases/${database}/${newName}/schema`
  360. );
  361. }}
  362. />
  363. ),
  364. },
  365. });
  366. }}
  367. tooltip={btnTrans('rename')}
  368. icon={<Icons.edit />}
  369. />
  370. <CopyButton
  371. sx={{
  372. '& svg': {
  373. width: 14,
  374. height: 14,
  375. },
  376. }}
  377. copyValue={collection.id}
  378. copyLabel={`${collectionTrans('collectionId')}: ${collection.id}`}
  379. />
  380. <RefreshButton
  381. sx={{
  382. '& svg': {
  383. width: 14,
  384. height: 14,
  385. },
  386. }}
  387. onClick={async () => {
  388. const res =
  389. await CollectionService.describeCollectionUnformatted(
  390. collection.collection_name
  391. );
  392. const json = JSON.stringify(res, null, 2);
  393. const blob = new Blob([json], {
  394. type: 'application/json',
  395. });
  396. const url = URL.createObjectURL(blob);
  397. const a = document.createElement('a');
  398. a.href = url;
  399. a.download = `${collection.collection_name}.json`;
  400. a.click();
  401. }}
  402. tooltip={btnTrans('downloadSchema')}
  403. icon={<Icons.download />}
  404. />
  405. <RefreshButton
  406. sx={{
  407. '& svg': {
  408. width: 14,
  409. height: 14,
  410. },
  411. }}
  412. onClick={() => {
  413. setDialog({
  414. open: true,
  415. type: 'custom',
  416. params: {
  417. component: (
  418. <DropCollectionDialog
  419. collections={[collection]}
  420. onDelete={() => {
  421. navigate(`/databases/${database}`);
  422. }}
  423. />
  424. ),
  425. },
  426. });
  427. }}
  428. tooltip={btnTrans('drop')}
  429. icon={<Icons.cross />}
  430. />
  431. <RefreshButton
  432. sx={{
  433. '& svg': {
  434. width: 14,
  435. height: 14,
  436. },
  437. }}
  438. tooltip={btnTrans('refresh')}
  439. onClick={async () => {
  440. await fetchCollection(collectionName);
  441. }}
  442. icon={<Icons.refresh />}
  443. />
  444. </ActionWrapper>
  445. </InfoValue>
  446. </InfoRow>
  447. <InfoRow>
  448. <InfoLabel>{collectionTrans('description')}</InfoLabel>
  449. <InfoValue>
  450. <Typography variant="body2">
  451. {collection?.description || '--'}
  452. </Typography>
  453. </InfoValue>
  454. </InfoRow>
  455. <InfoRow>
  456. <InfoLabel>{collectionTrans('createdTime')}</InfoLabel>
  457. <InfoValue>
  458. <Typography variant="body2">
  459. {new Date(collection.createdTime).toLocaleString()}
  460. </Typography>
  461. </InfoValue>
  462. </InfoRow>
  463. </Card>
  464. <Card>
  465. <InfoRow>
  466. <InfoLabel>{collectionTrans('status')}</InfoLabel>
  467. <InfoValue>
  468. <StatusAction
  469. status={collection.status}
  470. percentage={collection.loadedPercentage}
  471. collection={collection}
  472. showExtraAction={false}
  473. showLoadButton={true}
  474. createIndexElement={CreateIndexElement}
  475. />
  476. </InfoValue>
  477. </InfoRow>
  478. <InfoRow>
  479. <InfoLabel>
  480. {collectionTrans('replica')}
  481. <CustomToolTip title={collectionTrans('replicaTooltip')}>
  482. <Icons.question
  483. sx={{
  484. width: 12,
  485. height: 12,
  486. position: 'relative',
  487. top: '2px',
  488. right: '-4px',
  489. }}
  490. />
  491. </CustomToolTip>
  492. </InfoLabel>
  493. <InfoValue>
  494. <Typography variant="body2">
  495. {collection.loaded ? collection.replicas?.length : '...'}
  496. </Typography>
  497. {collection.loaded && enableModifyReplica && (
  498. <RefreshButton
  499. sx={{
  500. '& svg': {
  501. width: 12,
  502. height: 12,
  503. },
  504. }}
  505. tooltip={collectionTrans('modifyReplicaTooltip')}
  506. onClick={() => {
  507. setDialog({
  508. open: true,
  509. type: 'custom',
  510. params: {
  511. component: (
  512. <LoadCollectionDialog
  513. collection={collection}
  514. isModifyReplica={true}
  515. />
  516. ),
  517. },
  518. });
  519. }}
  520. icon={<Icons.settings />}
  521. />
  522. )}
  523. </InfoValue>
  524. </InfoRow>
  525. <InfoRow>
  526. <InfoLabel>
  527. {collection.loaded ? (
  528. collectionTrans('count')
  529. ) : (
  530. <>
  531. {collectionTrans('rowCount')}
  532. <CustomToolTip title={collectionTrans('entityCountInfo')}>
  533. <Icons.question
  534. sx={{
  535. width: 12,
  536. height: 12,
  537. position: 'relative',
  538. top: '2px',
  539. right: '-4px',
  540. }}
  541. />
  542. </CustomToolTip>
  543. </>
  544. )}
  545. </InfoLabel>
  546. <InfoValue>
  547. <Typography variant="body2">
  548. {formatNumber(Number(collection?.rowCount || '0'))}
  549. </Typography>
  550. </InfoValue>
  551. </InfoRow>
  552. </Card>
  553. <Card>
  554. <InfoRow>
  555. <InfoLabel>{collectionTrans('features')}</InfoLabel>
  556. <InfoValue>
  557. <Box className="features-wrapper">
  558. {isAutoIDEnabled && (
  559. <StyledChip
  560. sx={{ border: 'none' }}
  561. label={collectionTrans('autoId')}
  562. size="small"
  563. />
  564. )}
  565. <Tooltip
  566. title={
  567. collection.consistency_level
  568. ? consistencyTooltipsMap[
  569. collection.consistency_level
  570. ] || ''
  571. : ''
  572. }
  573. arrow
  574. >
  575. <StyledChip
  576. sx={{ border: 'none' }}
  577. label={`${collectionTrans('consistency')}: ${collection.consistency_level}`}
  578. size="small"
  579. />
  580. </Tooltip>
  581. <Tooltip title={collectionTrans('mmapTooltip')} arrow>
  582. <StyledChip
  583. label={collectionTrans('mmapSettings')}
  584. size="small"
  585. onDelete={async () => {
  586. setDialog({
  587. open: true,
  588. type: 'custom',
  589. params: {
  590. component: (
  591. <EditMmapDialog
  592. collection={collection}
  593. cb={async () => {
  594. fetchCollection(collectionName);
  595. }}
  596. />
  597. ),
  598. },
  599. });
  600. }}
  601. deleteIcon={
  602. <Icons.settings
  603. sx={{
  604. width: 12,
  605. height: 12,
  606. }}
  607. />
  608. }
  609. />
  610. </Tooltip>
  611. </Box>
  612. </InfoValue>
  613. </InfoRow>
  614. </Card>
  615. </InfoWrapper>
  616. )}
  617. <GridWrapper>
  618. <AttuGrid
  619. toolbarConfigs={[]}
  620. colDefinitions={colDefinitions}
  621. rows={fields}
  622. rowCount={fields.length}
  623. primaryKey="fieldID"
  624. showHoverStyle={false}
  625. isLoading={loading}
  626. openCheckBox={false}
  627. showPagination={false}
  628. labelDisplayedRows={getLabelDisplayedRows(
  629. commonTrans(`grid.${fields.length > 1 ? 'fields' : 'field'}`)
  630. )}
  631. />
  632. </GridWrapper>
  633. </Wrapper>
  634. );
  635. };
  636. export default Overview;