Databases.tsx 11 KB

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