Schema.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  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="body2"
  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 <DataTypeChip size="small" label={formatFieldType(f)} />;
  178. },
  179. label: collectionTrans('fieldType'),
  180. },
  181. {
  182. id: 'nullable',
  183. align: 'left',
  184. disablePadding: false,
  185. label: collectionTrans('nullable'),
  186. formatter(f) {
  187. return f.nullable ? (
  188. <Icons.check sx={{ fontSize: '11px', ml: 0.5 }} />
  189. ) : (
  190. <Icons.cross2 sx={{ fontSize: '11px', ml: 0.5 }} />
  191. );
  192. },
  193. },
  194. {
  195. id: 'default_value',
  196. align: 'left',
  197. disablePadding: false,
  198. label: collectionTrans('defaultValue'),
  199. formatter(f) {
  200. return f.default_value || '--';
  201. },
  202. },
  203. {
  204. id: 'index_name',
  205. align: 'left',
  206. disablePadding: true,
  207. label: indexTrans('indexName'),
  208. formatter(f) {
  209. return f.index?.index_name;
  210. },
  211. },
  212. {
  213. id: 'name',
  214. align: 'left',
  215. disablePadding: true,
  216. label: indexTrans('type'),
  217. notSort: true,
  218. formatter(f) {
  219. return (
  220. <IndexTypeElement
  221. field={f}
  222. collectionName={collectionName}
  223. cb={async () => {
  224. await fetchCollection(collectionName);
  225. }}
  226. />
  227. );
  228. },
  229. },
  230. {
  231. id: 'name',
  232. align: 'left',
  233. disablePadding: false,
  234. label: indexTrans('param'),
  235. notSort: true,
  236. formatter(f) {
  237. return f.index ? (
  238. <ParamWrapper>
  239. {f.index.indexParameterPairs.length > 0 ? (
  240. f.index.indexParameterPairs.map((p: any) =>
  241. p.value ? (
  242. <div key={p.key + p.value}>
  243. <span className="param">
  244. {`${p.key}:`}
  245. {p.value}
  246. </span>
  247. </div>
  248. ) : (
  249. ''
  250. )
  251. )
  252. ) : (
  253. <>--</>
  254. )}
  255. </ParamWrapper>
  256. ) : (
  257. <>--</>
  258. );
  259. },
  260. },
  261. {
  262. id: 'description',
  263. align: 'left',
  264. disablePadding: false,
  265. label: indexTrans('desc'),
  266. formatter(f) {
  267. return f.description || '--';
  268. },
  269. },
  270. ];
  271. // only show create index element when there is only one vector field
  272. let CreateIndexElement = null;
  273. if (
  274. collection &&
  275. collection.schema &&
  276. collection.schema.vectorFields.length === 1
  277. ) {
  278. CreateIndexElement = (
  279. <IndexTypeElement
  280. field={
  281. (collection.schema && collection.schema.vectorFields[0]) ||
  282. ({} as FieldObject)
  283. }
  284. collectionName={collectionName}
  285. cb={async () => {
  286. await fetchCollection(collectionName);
  287. }}
  288. />
  289. );
  290. }
  291. // enable modify replica if there are more than one query node
  292. const enableModifyReplica =
  293. data && data.queryNodes && data.queryNodes.length > 1;
  294. // if is autoID enabled
  295. const isAutoIDEnabled = collection?.schema?.fields.some(
  296. f => f.autoID === true
  297. );
  298. // check if collection is mmap enabled
  299. const isCollectionMmapEnabled = collection?.properties?.some((p: any) => {
  300. return p.key === 'mmap.enabled' && p.value === 'true';
  301. });
  302. // get loading state label
  303. return (
  304. <Wrapper>
  305. {collection && (
  306. <InfoWrapper>
  307. <Card>
  308. <InfoRow>
  309. <InfoLabel>{collectionTrans('name')}</InfoLabel>
  310. <InfoValue>
  311. <Tooltip title={collection.collection_name} arrow>
  312. <Typography
  313. variant="body1"
  314. sx={{ fontWeight: 500 }}
  315. className="truncate"
  316. >
  317. {collection.collection_name}
  318. </Typography>
  319. </Tooltip>
  320. <ActionWrapper>
  321. <RefreshButton
  322. sx={{
  323. '& svg': {
  324. width: 14,
  325. height: 14,
  326. },
  327. }}
  328. onClick={async () => {
  329. setDialog({
  330. open: true,
  331. type: 'custom',
  332. params: {
  333. component: (
  334. <RenameCollectionDialog
  335. collection={collection}
  336. cb={async newName => {
  337. await fetchCollection(newName);
  338. navigate(
  339. `/databases/${database}/${newName}/schema`
  340. );
  341. }}
  342. />
  343. ),
  344. },
  345. });
  346. }}
  347. tooltip={btnTrans('rename')}
  348. icon={<Icons.edit />}
  349. />
  350. <CopyButton
  351. sx={{
  352. '& svg': {
  353. width: 14,
  354. height: 14,
  355. },
  356. }}
  357. copyValue={collection.id}
  358. copyLabel={`${collectionTrans('collectionId')}: ${collection.id}`}
  359. />
  360. <RefreshButton
  361. sx={{
  362. '& svg': {
  363. width: 14,
  364. height: 14,
  365. },
  366. }}
  367. onClick={async () => {
  368. const res =
  369. await CollectionService.describeCollectionUnformatted(
  370. collection.collection_name
  371. );
  372. const json = JSON.stringify(res, null, 2);
  373. const blob = new Blob([json], {
  374. type: 'application/json',
  375. });
  376. const url = URL.createObjectURL(blob);
  377. const a = document.createElement('a');
  378. a.href = url;
  379. a.download = `${collection.collection_name}.json`;
  380. a.click();
  381. }}
  382. tooltip={btnTrans('downloadSchema')}
  383. icon={<Icons.download />}
  384. />
  385. <RefreshButton
  386. sx={{
  387. '& svg': {
  388. width: 14,
  389. height: 14,
  390. },
  391. }}
  392. onClick={() => {
  393. setDialog({
  394. open: true,
  395. type: 'custom',
  396. params: {
  397. component: (
  398. <DropCollectionDialog
  399. collections={[collection]}
  400. onDelete={() => {
  401. navigate(`/databases/${database}`);
  402. }}
  403. />
  404. ),
  405. },
  406. });
  407. }}
  408. tooltip={btnTrans('drop')}
  409. icon={<Icons.cross />}
  410. />
  411. <RefreshButton
  412. sx={{
  413. '& svg': {
  414. width: 14,
  415. height: 14,
  416. },
  417. }}
  418. tooltip={btnTrans('refresh')}
  419. onClick={async () => {
  420. await fetchCollection(collectionName);
  421. }}
  422. icon={<Icons.refresh />}
  423. />
  424. </ActionWrapper>
  425. </InfoValue>
  426. </InfoRow>
  427. <InfoRow>
  428. <InfoLabel>{collectionTrans('description')}</InfoLabel>
  429. <InfoValue>
  430. <Typography variant="body2">
  431. {collection?.description || '--'}
  432. </Typography>
  433. </InfoValue>
  434. </InfoRow>
  435. <InfoRow>
  436. <InfoLabel>{collectionTrans('createdTime')}</InfoLabel>
  437. <InfoValue>
  438. <Typography variant="body2">
  439. {new Date(collection.createdTime).toLocaleString()}
  440. </Typography>
  441. </InfoValue>
  442. </InfoRow>
  443. </Card>
  444. <Card>
  445. <InfoRow>
  446. <InfoLabel>{collectionTrans('status')}</InfoLabel>
  447. <InfoValue>
  448. <StatusAction
  449. status={collection.status}
  450. percentage={collection.loadedPercentage}
  451. collection={collection}
  452. showExtraAction={false}
  453. showLoadButton={true}
  454. createIndexElement={CreateIndexElement}
  455. />
  456. </InfoValue>
  457. </InfoRow>
  458. <InfoRow>
  459. <InfoLabel>
  460. {collectionTrans('replica')}
  461. <CustomToolTip title={collectionTrans('replicaTooltip')}>
  462. <Icons.question
  463. sx={{
  464. width: 12,
  465. height: 12,
  466. position: 'relative',
  467. top: '2px',
  468. right: '-4px',
  469. }}
  470. />
  471. </CustomToolTip>
  472. </InfoLabel>
  473. <InfoValue>
  474. <Typography variant="body2">
  475. {collection.loaded ? collection.replicas?.length : '...'}
  476. </Typography>
  477. {collection.loaded && enableModifyReplica && (
  478. <RefreshButton
  479. sx={{
  480. '& svg': {
  481. width: 12,
  482. height: 12,
  483. },
  484. }}
  485. tooltip={collectionTrans('modifyReplicaTooltip')}
  486. onClick={() => {
  487. setDialog({
  488. open: true,
  489. type: 'custom',
  490. params: {
  491. component: (
  492. <LoadCollectionDialog
  493. collection={collection}
  494. isModifyReplica={true}
  495. />
  496. ),
  497. },
  498. });
  499. }}
  500. icon={<Icons.settings />}
  501. />
  502. )}
  503. </InfoValue>
  504. </InfoRow>
  505. <InfoRow>
  506. <InfoLabel>
  507. {collection.loaded ? (
  508. collectionTrans('count')
  509. ) : (
  510. <>
  511. {collectionTrans('rowCount')}
  512. <CustomToolTip title={collectionTrans('entityCountInfo')}>
  513. <Icons.question
  514. sx={{
  515. width: 12,
  516. height: 12,
  517. position: 'relative',
  518. top: '2px',
  519. right: '-4px',
  520. }}
  521. />
  522. </CustomToolTip>
  523. </>
  524. )}
  525. </InfoLabel>
  526. <InfoValue>
  527. <Typography variant="body2">
  528. {formatNumber(Number(collection?.rowCount || '0'))}
  529. </Typography>
  530. </InfoValue>
  531. </InfoRow>
  532. </Card>
  533. <Card>
  534. <InfoRow>
  535. <InfoLabel>{collectionTrans('features')}</InfoLabel>
  536. <InfoValue>
  537. <Box className="features-wrapper">
  538. {isAutoIDEnabled && (
  539. <StyledChip
  540. sx={{ border: 'none' }}
  541. label={collectionTrans('autoId')}
  542. size="small"
  543. />
  544. )}
  545. <Tooltip
  546. title={
  547. collection.consistency_level
  548. ? consistencyTooltipsMap[
  549. collection.consistency_level
  550. ] || ''
  551. : ''
  552. }
  553. arrow
  554. >
  555. <StyledChip
  556. sx={{ border: 'none' }}
  557. label={`${collectionTrans('consistency')}: ${collection.consistency_level}`}
  558. size="small"
  559. />
  560. </Tooltip>
  561. <Tooltip title={collectionTrans('mmapTooltip')} arrow>
  562. <StyledChip
  563. label={collectionTrans('mmapSettings')}
  564. size="small"
  565. onDelete={async () => {
  566. setDialog({
  567. open: true,
  568. type: 'custom',
  569. params: {
  570. component: (
  571. <EditMmapDialog
  572. collection={collection}
  573. cb={async () => {
  574. fetchCollection(collectionName);
  575. }}
  576. />
  577. ),
  578. },
  579. });
  580. }}
  581. deleteIcon={
  582. <Icons.settings
  583. sx={{
  584. width: 12,
  585. height: 12,
  586. }}
  587. />
  588. }
  589. />
  590. </Tooltip>
  591. </Box>
  592. </InfoValue>
  593. </InfoRow>
  594. </Card>
  595. </InfoWrapper>
  596. )}
  597. <GridWrapper>
  598. <AttuGrid
  599. toolbarConfigs={[]}
  600. colDefinitions={colDefinitions}
  601. rows={fields}
  602. rowCount={fields.length}
  603. primaryKey="fieldID"
  604. showHoverStyle={false}
  605. isLoading={loading}
  606. rowHeight={44}
  607. tableHeaderHeight={44}
  608. openCheckBox={false}
  609. showPagination={false}
  610. labelDisplayedRows={getLabelDisplayedRows(
  611. commonTrans(`grid.${fields.length > 1 ? 'fields' : 'field'}`)
  612. )}
  613. />
  614. </GridWrapper>
  615. </Wrapper>
  616. );
  617. };
  618. export default Overview;