Databases.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import { useContext, useEffect, useState, useRef } from 'react';
  2. import { useParams } from 'react-router-dom';
  3. import { useNavigationHook } from '@/hooks';
  4. import DatabaseTree from '@/pages/databases/tree';
  5. import { dataContext } from '@/context';
  6. import StatusIcon from '@/components/status/StatusIcon';
  7. import { ConsistencyLevelEnum, DYNAMIC_FIELD } from '@/consts';
  8. import { ROUTE_PATHS } from '@/config/routes';
  9. import { LoadingType } from '@/components/status/StatusIcon';
  10. import type { SearchParams, QueryState } from './types';
  11. import { DatabasesTab } from './DatabasesTab';
  12. import { CollectionsTabs } from './CollectionsTab';
  13. import { styled } from '@mui/material/styles';
  14. import { Box } from '@mui/material';
  15. const DEFAULT_TREE_WIDTH = 230;
  16. const MAX_TREE_WIDTH = 500; // Maximum width the tree can be dragged to
  17. const PageWrapper = styled(Box)(({ theme }) => ({
  18. display: 'flex',
  19. flexDirection: 'row',
  20. padding: theme.spacing(1.5),
  21. height: 'calc(100vh - 64px)',
  22. overflow: 'hidden',
  23. '&.dragging': {
  24. cursor: 'ew-resize',
  25. '& .tree': {
  26. pointerEvents: 'none',
  27. userSelect: 'none',
  28. },
  29. '& .tab': {
  30. pointerEvents: 'none',
  31. userSelect: 'none',
  32. },
  33. '& .dragger': {
  34. background: theme.palette.divider,
  35. },
  36. },
  37. }));
  38. const TreeSection = styled(Box)(({ theme }) => ({
  39. flexGrow: 0,
  40. flexShrink: 0,
  41. height: 'calc(100vh - 54px)',
  42. overflowY: 'auto',
  43. overflowX: 'hidden',
  44. boxSizing: 'border-box',
  45. paddingRight: 8,
  46. position: 'relative',
  47. }));
  48. const Dragger = styled(Box)(({ theme }) => ({
  49. pointerEvents: 'auto',
  50. position: 'absolute',
  51. top: 0,
  52. right: 0,
  53. width: 2,
  54. height: '100%',
  55. background: 'transparent',
  56. cursor: 'ew-resize',
  57. '&:hover': {
  58. width: 2,
  59. cursor: 'ew-resize',
  60. background: theme.palette.divider,
  61. },
  62. }));
  63. const TabSection = styled(Box)(({ theme }) => ({
  64. flexGrow: 1,
  65. flexShrink: 1,
  66. overflow: 'hidden',
  67. padding: theme.spacing(0, 1.5),
  68. border: `1px solid ${theme.palette.divider}`,
  69. borderRadius: 4,
  70. height: '100%',
  71. display: 'flex',
  72. flexDirection: 'column',
  73. backgroundColor: theme.palette.background.paper,
  74. }));
  75. // Databases page(tree and tabs)
  76. const Databases = () => {
  77. // context
  78. const { database, collections, loading, ui, setUIPref } =
  79. useContext(dataContext);
  80. // UI state
  81. const [searchParams, setSearchParams] = useState<SearchParams[]>([]);
  82. const [queryState, setQueryState] = useState<QueryState[]>([]);
  83. // tree ref
  84. const [isDragging, setIsDragging] = useState(false);
  85. const treeRef = useRef<HTMLDivElement>(null);
  86. const draggerRef = useRef<HTMLDivElement>(null);
  87. // support dragging tree width
  88. useEffect(() => {
  89. // local tree width
  90. let treeWidth = 0;
  91. const handleMouseMove = (e: MouseEvent) => {
  92. requestAnimationFrame(() => {
  93. // get mouse position
  94. let mouseX = e.clientX - treeRef.current!.offsetLeft;
  95. // set min and max width
  96. treeWidth = Math.max(1, Math.min(mouseX, MAX_TREE_WIDTH));
  97. // set tree width
  98. setUIPref({ tree: { width: treeWidth } });
  99. setIsDragging(true);
  100. });
  101. };
  102. const handleMouseUp = () => {
  103. // set dragging false
  104. setIsDragging(false);
  105. // highlight dragger alwasy if width === 1
  106. if (draggerRef.current) {
  107. draggerRef.current.classList.toggle('tree-collapsed', treeWidth === 1);
  108. }
  109. document.removeEventListener('mousemove', handleMouseMove);
  110. };
  111. const handleMouseDown = (e: MouseEvent) => {
  112. const t = e.target as HTMLDivElement;
  113. if (t && t.dataset.id === 'dragger') {
  114. // set dragging true
  115. setIsDragging(true);
  116. document.addEventListener('mousemove', handleMouseMove);
  117. document.addEventListener('mouseup', handleMouseUp);
  118. e.stopPropagation();
  119. }
  120. };
  121. // add event listener
  122. document.addEventListener('mousedown', handleMouseDown);
  123. return () => {
  124. // remove event listener
  125. document.removeEventListener('mousedown', handleMouseDown);
  126. // set dragging false
  127. setIsDragging(false);
  128. };
  129. }, [isDragging]);
  130. // double click on the dragger, recover default
  131. const handleDoubleClick = () => {
  132. draggerRef.current!.classList.toggle('tree-collapsed', false);
  133. setUIPref({ tree: { width: DEFAULT_TREE_WIDTH } });
  134. };
  135. // init search params and query state
  136. useEffect(() => {
  137. collections.forEach(c => {
  138. // find search params for the collection
  139. const searchParam = searchParams.find(
  140. s => s.collection.collection_name === c.collection_name
  141. );
  142. // if search params not found, and the schema is ready, create new search params
  143. if (!searchParam && c.schema) {
  144. setSearchParams(prevParams => {
  145. const scalarFields = c.schema.scalarFields.map(s => s.name);
  146. return [
  147. ...prevParams,
  148. {
  149. collection: c,
  150. partitions: [],
  151. searchParams: c.schema.vectorFields.map(v => {
  152. return {
  153. anns_field: v.name,
  154. params: {},
  155. data: '',
  156. expanded: c.schema.vectorFields.length === 1,
  157. field: v,
  158. selected: c.schema.vectorFields.length === 1,
  159. };
  160. }),
  161. globalParams: {
  162. topK: 15,
  163. consistency_level: ConsistencyLevelEnum.Bounded,
  164. filter: '',
  165. rerank: 'rrf',
  166. rrfParams: { k: 60 },
  167. weightedParams: {
  168. weights: Array(c.schema.vectorFields.length).fill(0.5),
  169. },
  170. output_fields: c.schema.enable_dynamic_field
  171. ? [...scalarFields, DYNAMIC_FIELD]
  172. : scalarFields,
  173. },
  174. searchResult: null,
  175. graphData: { nodes: [], links: [] },
  176. searchLatency: 0,
  177. },
  178. ];
  179. });
  180. } else {
  181. // update collection
  182. setSearchParams(prevParams => {
  183. return prevParams.map(s => {
  184. if (s.collection.collection_name === c.collection_name) {
  185. // update field in search params
  186. const searchParams = s.searchParams.map(sp => {
  187. const field = c.schema?.vectorFields.find(
  188. v => v.name === sp.anns_field
  189. );
  190. if (field) {
  191. return { ...sp, field };
  192. }
  193. return sp;
  194. });
  195. // update collection
  196. const collection = c;
  197. return { ...s, searchParams, collection };
  198. }
  199. return s;
  200. });
  201. });
  202. }
  203. // find query state for the collection
  204. const query = queryState.find(
  205. q => q.collection.collection_name === c.collection_name
  206. );
  207. // if query state not found, and the schema is ready, create new query state
  208. if (!query && c.schema) {
  209. setQueryState(prevState => {
  210. const fields = [...c.schema.fields].filter(
  211. f => !f.is_function_output
  212. );
  213. return [
  214. ...prevState,
  215. {
  216. collection: c,
  217. expr: '',
  218. fields: fields,
  219. outputFields: fields.map(f => f.name),
  220. consistencyLevel: ConsistencyLevelEnum.Bounded,
  221. tick: 0,
  222. },
  223. ];
  224. });
  225. } else {
  226. // update collection
  227. setQueryState(prevState => {
  228. return prevState.map(q => {
  229. if (q.collection.collection_name === c.collection_name) {
  230. // update collection
  231. const collection = c;
  232. return { ...q, collection };
  233. }
  234. return q;
  235. });
  236. });
  237. }
  238. });
  239. // delete search params for the collection that is not in the collections
  240. setSearchParams(prevParams => {
  241. return prevParams.filter(s =>
  242. collections.find(
  243. c => c.collection_name === s.collection.collection_name
  244. )
  245. );
  246. });
  247. // delete query state for the collection that is not in the collections
  248. setQueryState(prevState => {
  249. return prevState.filter(q =>
  250. collections.find(
  251. c => c.collection_name === q.collection.collection_name
  252. )
  253. );
  254. });
  255. }, [collections]);
  256. // get current collection from url
  257. const params = useParams();
  258. const {
  259. databaseName = '',
  260. collectionName = '',
  261. collectionPage = '',
  262. databasePage = '',
  263. } = params;
  264. // update navigation
  265. useNavigationHook(ROUTE_PATHS.DATABASES, {
  266. collectionName,
  267. databaseName,
  268. });
  269. const setCollectionSearchParams = (params: SearchParams) => {
  270. setSearchParams(prevParams => {
  271. return prevParams.map(s => {
  272. if (
  273. s.collection.collection_name === params.collection.collection_name
  274. ) {
  275. return { ...params };
  276. }
  277. return s;
  278. });
  279. });
  280. };
  281. const setCollectionQueryState = (state: QueryState) => {
  282. setQueryState(prevState => {
  283. return prevState.map(q => {
  284. if (q.collection.collection_name === state.collection.collection_name) {
  285. return { ...state };
  286. }
  287. return q;
  288. });
  289. });
  290. };
  291. // render
  292. return (
  293. <PageWrapper className={isDragging ? 'dragging' : ''}>
  294. <TreeSection
  295. className="tree"
  296. ref={treeRef}
  297. style={{ width: ui.tree.width }}
  298. >
  299. {loading ? (
  300. <StatusIcon type={LoadingType.CREATING} />
  301. ) : (
  302. <DatabaseTree
  303. key="collections"
  304. collections={collections}
  305. database={database}
  306. params={params}
  307. />
  308. )}
  309. <Dragger
  310. className="dragger"
  311. data-id="dragger"
  312. onDoubleClick={handleDoubleClick}
  313. ref={draggerRef}
  314. />
  315. </TreeSection>
  316. {!collectionName && (
  317. <TabSection className="tab">
  318. <DatabasesTab
  319. databasePage={databasePage}
  320. databaseName={databaseName}
  321. tabClass="tab"
  322. />
  323. </TabSection>
  324. )}
  325. {collectionName && (
  326. <TabSection className="tab">
  327. <CollectionsTabs
  328. collectionPage={collectionPage}
  329. collectionName={collectionName}
  330. tabClass="tab"
  331. searchParams={
  332. searchParams.find(
  333. s => s.collection.collection_name === collectionName
  334. )!
  335. }
  336. setSearchParams={setCollectionSearchParams}
  337. queryState={
  338. queryState.find(
  339. q => q.collection.collection_name === collectionName
  340. )!
  341. }
  342. setQueryState={setCollectionQueryState}
  343. collections={collections}
  344. />
  345. </TabSection>
  346. )}
  347. </PageWrapper>
  348. );
  349. };
  350. export default Databases;