Databases.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. import { useContext, useEffect, useState, useRef } from 'react';
  2. import { useParams } from 'react-router-dom';
  3. import { useTranslation } from 'react-i18next';
  4. import { Theme } from '@mui/material';
  5. import { useNavigationHook } from '@/hooks';
  6. import { ALL_ROUTER_TYPES } from '@/router/consts';
  7. import RouteTabList from '@/components/customTabList/RouteTabList';
  8. import DatabaseTree from '@/pages/databases/tree';
  9. import Partitions from './collections/partitions/Partitions';
  10. import Schema from './collections/schema/Schema';
  11. import Data from './collections/data/CollectionData';
  12. import Segments from './collections/segments/Segments';
  13. import Properties from './collections/properties/Properties';
  14. import Search from './collections/search/Search';
  15. import { dataContext, authContext } from '@/context';
  16. import Collections from './collections/Collections';
  17. import StatusIcon, { LoadingType } from '@/components/status/StatusIcon';
  18. import { ConsistencyLevelEnum, DYNAMIC_FIELD } from '@/consts';
  19. import { makeStyles } from '@mui/styles';
  20. import type { SearchParams, QueryState } from './types';
  21. import type { CollectionObject, CollectionFullObject } from '@server/types';
  22. import type { ITab } from '@/components/customTabList/Types';
  23. const DEFAULT_TREE_WIDTH = 230;
  24. const useStyles = makeStyles((theme: Theme) => ({
  25. wrapper: {
  26. flexDirection: 'row',
  27. padding: theme.spacing(0),
  28. '&.dragging': {
  29. cursor: 'ew-resize',
  30. '& $tree': {
  31. pointerEvents: 'none',
  32. userSelect: 'none',
  33. },
  34. '& $tab': {
  35. pointerEvents: 'none',
  36. userSelect: 'none',
  37. },
  38. '& $dragger': {
  39. background: theme.palette.divider,
  40. },
  41. },
  42. },
  43. tree: {
  44. boxShadow: 'none',
  45. flexGrow: 0,
  46. flexShrink: 0,
  47. height: 'calc(100vh - 90px)',
  48. overflowY: 'auto',
  49. overflowX: 'hidden',
  50. boxSizing: 'border-box',
  51. paddingRight: 8,
  52. position: 'relative',
  53. },
  54. dragger: {
  55. pointerEvents: 'auto',
  56. position: 'absolute',
  57. top: 0,
  58. right: 0,
  59. width: 2,
  60. height: '100%',
  61. background: 'transparent',
  62. cursor: 'ew-resize',
  63. '&:hover': {
  64. width: 2,
  65. cursor: 'ew-resize',
  66. background: theme.palette.divider,
  67. },
  68. },
  69. tab: {
  70. flexGrow: 1,
  71. flexShrink: 1,
  72. overflowX: 'auto',
  73. padding: theme.spacing(0, 2),
  74. border: `1px solid ${theme.palette.divider}`,
  75. borderRadius: 8,
  76. boxShadow: '0px 6px 30px rgba(0, 0, 0, 0.1)',
  77. },
  78. }));
  79. // Databases page(tree and tabs)
  80. const Databases = () => {
  81. // context
  82. const { database, collections, loading, ui, setUIPref } =
  83. useContext(dataContext);
  84. // UI state
  85. const [searchParams, setSearchParams] = useState<SearchParams[]>([]);
  86. const [queryState, setQueryState] = useState<QueryState[]>([]);
  87. // tree ref
  88. const [isDragging, setIsDragging] = useState(false);
  89. const treeRef = useRef<HTMLDivElement>(null);
  90. const draggerRef = useRef<HTMLDivElement>(null);
  91. // support dragging tree width
  92. useEffect(() => {
  93. // local tree width
  94. let treeWidth = 0;
  95. const handleMouseMove = (e: MouseEvent) => {
  96. requestAnimationFrame(() => {
  97. // get mouse position
  98. let mouseX = e.clientX - treeRef.current!.offsetLeft;
  99. // set min and max width
  100. treeWidth = Math.max(1, Math.min(mouseX, DEFAULT_TREE_WIDTH));
  101. // set tree width
  102. setUIPref({ tree: { width: treeWidth } });
  103. setIsDragging(true);
  104. });
  105. };
  106. const handleMouseUp = () => {
  107. // set dragging false
  108. setIsDragging(false);
  109. // highlight dragger alwasy if width === 1
  110. draggerRef.current!.classList.toggle('tree-collapsed', treeWidth === 1);
  111. document.removeEventListener('mousemove', handleMouseMove);
  112. };
  113. const handleMouseDown = (e: MouseEvent) => {
  114. const t = e.target as HTMLDivElement;
  115. if (t && t.dataset.id === 'dragger') {
  116. // set dragging true
  117. setIsDragging(true);
  118. document.addEventListener('mousemove', handleMouseMove);
  119. document.addEventListener('mouseup', handleMouseUp);
  120. e.stopPropagation();
  121. }
  122. };
  123. // add event listener
  124. document.addEventListener('mousedown', handleMouseDown);
  125. return () => {
  126. // remove event listener
  127. document.removeEventListener('mousedown', handleMouseDown);
  128. // set dragging false
  129. setIsDragging(false);
  130. };
  131. }, [isDragging]);
  132. // double click on the dragger, recover default
  133. const handleDoubleClick = () => {
  134. draggerRef.current!.classList.toggle('tree-collapsed', false);
  135. setUIPref({ tree: { width: DEFAULT_TREE_WIDTH } });
  136. };
  137. // init search params and query state
  138. useEffect(() => {
  139. collections.forEach(c => {
  140. // find search params for the collection
  141. const searchParam = searchParams.find(
  142. s => s.collection.collection_name === c.collection_name
  143. );
  144. // if search params not found, and the schema is ready, create new search params
  145. if (!searchParam && c.schema) {
  146. setSearchParams(prevParams => {
  147. const scalarFields = c.schema.scalarFields.map(s => s.name);
  148. return [
  149. ...prevParams,
  150. {
  151. collection: c,
  152. partitions: [],
  153. searchParams: c.schema.vectorFields.map(v => {
  154. return {
  155. anns_field: v.name,
  156. params: {},
  157. data: '',
  158. expanded: c.schema.vectorFields.length === 1,
  159. field: v,
  160. selected: c.schema.vectorFields.length === 1,
  161. };
  162. }),
  163. globalParams: {
  164. topK: 15,
  165. consistency_level: ConsistencyLevelEnum.Bounded,
  166. filter: '',
  167. rerank: 'rrf',
  168. rrfParams: { k: 60 },
  169. weightedParams: {
  170. weights: Array(c.schema.vectorFields.length).fill(0.5),
  171. },
  172. output_fields: c.schema.enable_dynamic_field
  173. ? [...scalarFields, DYNAMIC_FIELD]
  174. : scalarFields,
  175. },
  176. searchResult: null,
  177. graphData: { nodes: [], links: [] },
  178. searchLatency: 0,
  179. },
  180. ];
  181. });
  182. } else {
  183. // update collection
  184. setSearchParams(prevParams => {
  185. return prevParams.map(s => {
  186. if (s.collection.collection_name === c.collection_name) {
  187. // update field in search params
  188. const searchParams = s.searchParams.map(sp => {
  189. const field = c.schema?.vectorFields.find(
  190. v => v.name === sp.anns_field
  191. );
  192. if (field) {
  193. return { ...sp, field };
  194. }
  195. return sp;
  196. });
  197. // update collection
  198. const collection = c;
  199. return { ...s, searchParams, collection };
  200. }
  201. return s;
  202. });
  203. });
  204. }
  205. // find query state for the collection
  206. const query = queryState.find(
  207. q => q.collection.collection_name === c.collection_name
  208. );
  209. // if query state not found, and the schema is ready, create new query state
  210. if (!query && c.schema) {
  211. setQueryState(prevState => {
  212. const fields = [...c.schema.fields, ...c.schema.dynamicFields].filter(
  213. f => !f.is_function_output
  214. );
  215. return [
  216. ...prevState,
  217. {
  218. collection: c,
  219. expr: '',
  220. fields: fields,
  221. outputFields: fields.map(f => f.name),
  222. consistencyLevel: ConsistencyLevelEnum.Bounded,
  223. tick: 0,
  224. },
  225. ];
  226. });
  227. } else {
  228. // update collection
  229. setQueryState(prevState => {
  230. return prevState.map(q => {
  231. if (q.collection.collection_name === c.collection_name) {
  232. // update collection
  233. const collection = c;
  234. return { ...q, collection };
  235. }
  236. return q;
  237. });
  238. });
  239. }
  240. });
  241. // delete search params for the collection that is not in the collections
  242. setSearchParams(prevParams => {
  243. return prevParams.filter(s =>
  244. collections.find(
  245. c => c.collection_name === s.collection.collection_name
  246. )
  247. );
  248. });
  249. // delete query state for the collection that is not in the collections
  250. setQueryState(prevState => {
  251. return prevState.filter(q =>
  252. collections.find(
  253. c => c.collection_name === q.collection.collection_name
  254. )
  255. );
  256. });
  257. }, [collections]);
  258. // get current collection from url
  259. const params = useParams();
  260. const {
  261. databaseName = '',
  262. collectionName = '',
  263. collectionPage = '',
  264. databasePage = '',
  265. } = params;
  266. // get style
  267. const classes = useStyles();
  268. // update navigation
  269. useNavigationHook(ALL_ROUTER_TYPES.DATABASES, {
  270. collectionName,
  271. databaseName,
  272. });
  273. const setCollectionSearchParams = (params: SearchParams) => {
  274. setSearchParams(prevParams => {
  275. return prevParams.map(s => {
  276. if (
  277. s.collection.collection_name === params.collection.collection_name
  278. ) {
  279. return { ...params };
  280. }
  281. return s;
  282. });
  283. });
  284. };
  285. const setCollectionQueryState = (state: QueryState) => {
  286. setQueryState(prevState => {
  287. return prevState.map(q => {
  288. if (q.collection.collection_name === state.collection.collection_name) {
  289. return { ...state };
  290. }
  291. return q;
  292. });
  293. });
  294. };
  295. // render
  296. return (
  297. <section
  298. className={`page-wrapper ${classes.wrapper} ${
  299. isDragging ? 'dragging' : ''
  300. }`}
  301. >
  302. <section
  303. className={classes.tree}
  304. ref={treeRef}
  305. style={{ width: ui.tree.width }}
  306. >
  307. {loading ? (
  308. <StatusIcon type={LoadingType.CREATING} />
  309. ) : (
  310. <DatabaseTree
  311. key="collections"
  312. collections={collections}
  313. database={database}
  314. params={params}
  315. />
  316. )}
  317. <div
  318. className={classes.dragger}
  319. data-id="dragger"
  320. onDoubleClick={handleDoubleClick}
  321. ref={draggerRef}
  322. ></div>
  323. </section>
  324. {!collectionName && (
  325. <DatabasesTab
  326. databasePage={databasePage}
  327. databaseName={databaseName}
  328. tabClass={classes.tab}
  329. />
  330. )}
  331. {collectionName && (
  332. <CollectionTabs
  333. collectionPage={collectionPage}
  334. collectionName={collectionName}
  335. tabClass={classes.tab}
  336. searchParams={
  337. searchParams.find(
  338. s => s.collection.collection_name === collectionName
  339. )!
  340. }
  341. setSearchParams={setCollectionSearchParams}
  342. queryState={
  343. queryState.find(
  344. q => q.collection.collection_name === collectionName
  345. )!
  346. }
  347. setQueryState={setCollectionQueryState}
  348. collections={collections}
  349. />
  350. )}
  351. </section>
  352. );
  353. };
  354. // Database tab pages
  355. const DatabasesTab = (props: {
  356. databasePage: string; // current database page
  357. databaseName: string;
  358. tabClass: string; // tab class
  359. }) => {
  360. // context
  361. const { isManaged } = useContext(authContext);
  362. const { databaseName, tabClass, databasePage } = props;
  363. const { t: collectionTrans } = useTranslation('collection');
  364. const dbTab: ITab[] = [
  365. {
  366. label: collectionTrans('collections'),
  367. component: <Collections />,
  368. path: `collections`,
  369. },
  370. ];
  371. if (!isManaged) {
  372. dbTab.push({
  373. label: collectionTrans('properties'),
  374. component: <Properties type="database" target={databaseName} />,
  375. path: `properties`,
  376. });
  377. }
  378. const actionDbTab = dbTab.findIndex(t => t.path === databasePage);
  379. return (
  380. <RouteTabList
  381. tabs={dbTab}
  382. wrapperClass={tabClass}
  383. activeIndex={actionDbTab !== -1 ? actionDbTab : 0}
  384. />
  385. );
  386. };
  387. // Collection tab pages
  388. const CollectionTabs = (props: {
  389. collectionPage: string; // current collection page
  390. collectionName: string; // current collection name
  391. tabClass: string; // tab class
  392. collections: CollectionObject[]; // collections
  393. searchParams: SearchParams; // search params
  394. setSearchParams: (params: SearchParams) => void; // set search params
  395. queryState: QueryState; // query state
  396. setQueryState: (state: QueryState) => void; // set query state
  397. }) => {
  398. // props
  399. const {
  400. collectionPage,
  401. collectionName,
  402. tabClass,
  403. collections,
  404. searchParams,
  405. setSearchParams,
  406. queryState,
  407. setQueryState,
  408. } = props;
  409. // context
  410. const { isManaged } = useContext(authContext);
  411. // i18n
  412. const { t: collectionTrans } = useTranslation('collection');
  413. const collection = collections.find(
  414. i => i.collection_name === collectionName
  415. ) as CollectionFullObject;
  416. // collection tabs
  417. const collectionTabs: ITab[] = [
  418. {
  419. label: collectionTrans('schemaTab'),
  420. component: <Schema />,
  421. path: `schema`,
  422. },
  423. {
  424. label: collectionTrans('searchTab'),
  425. component: (
  426. <Search
  427. collections={collections}
  428. collectionName={collectionName}
  429. searchParams={searchParams}
  430. setSearchParams={setSearchParams}
  431. />
  432. ),
  433. path: `search`,
  434. },
  435. {
  436. label: collectionTrans('dataTab'),
  437. component: <Data queryState={queryState} setQueryState={setQueryState} />,
  438. path: `data`,
  439. },
  440. {
  441. label: collectionTrans('partitionTab'),
  442. component: <Partitions />,
  443. path: `partitions`,
  444. },
  445. ];
  446. if (!isManaged) {
  447. collectionTabs.push(
  448. {
  449. label: collectionTrans('segmentsTab'),
  450. component: <Segments />,
  451. path: `segments`,
  452. },
  453. {
  454. label: collectionTrans('propertiesTab'),
  455. component: <Properties type="collection" target={collection} />,
  456. path: `properties`,
  457. }
  458. );
  459. }
  460. // get active collection tab
  461. const activeColTab = collectionTabs.findIndex(t => t.path === collectionPage);
  462. return (
  463. <RouteTabList
  464. tabs={collectionTabs}
  465. wrapperClass={tabClass}
  466. activeIndex={activeColTab !== -1 ? activeColTab : 0}
  467. />
  468. );
  469. };
  470. export default Databases;