Data.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. import {
  2. createContext,
  3. useCallback,
  4. useContext,
  5. useEffect,
  6. useState,
  7. useRef,
  8. } from 'react';
  9. import { io, Socket } from 'socket.io-client';
  10. import { authContext } from '@/context';
  11. import {
  12. CollectionService,
  13. MilvusService,
  14. DatabaseService,
  15. isElectron,
  16. url,
  17. } from '@/http';
  18. import { WS_EVENTS, WS_EVENTS_TYPE, LOADING_STATE } from '@server/utils/Const';
  19. import { DEFAULT_TREE_WIDTH, ATTU_UI_TREE_WIDTH } from '@/consts';
  20. import { checkIndexing, checkLoading } from '@server/utils/Shared';
  21. import type {
  22. IndexCreateParam,
  23. IndexManageParam,
  24. } from '@/pages/databases/collections/schema/Types';
  25. import type { DataContextType } from './Types';
  26. import type {
  27. CollectionObject,
  28. CollectionFullObject,
  29. DatabaseObject,
  30. ResStatus,
  31. } from '@server/types';
  32. export const dataContext = createContext<DataContextType>({
  33. loading: true,
  34. loadingDatabases: true,
  35. collections: [],
  36. setCollections: () => {},
  37. database: '',
  38. setDatabase: () => {},
  39. databases: [],
  40. setDatabaseList: () => {},
  41. createDatabase: async () => {},
  42. dropDatabase: async () => {},
  43. fetchDatabases: async () => {
  44. return [];
  45. },
  46. fetchCollections: async () => {},
  47. fetchCollection: async () => {
  48. return {} as CollectionFullObject;
  49. },
  50. createCollection: async () => {
  51. return {} as CollectionFullObject;
  52. },
  53. loadCollection: async () => {},
  54. releaseCollection: async () => {},
  55. renameCollection: async () => {
  56. return {} as CollectionFullObject;
  57. },
  58. duplicateCollection: async () => {
  59. return {} as CollectionFullObject;
  60. },
  61. dropCollection: async () => {
  62. return {
  63. error_code: -1,
  64. reason: '',
  65. };
  66. },
  67. createIndex: async () => {
  68. return {} as CollectionFullObject;
  69. },
  70. dropIndex: async () => {
  71. return {} as CollectionFullObject;
  72. },
  73. createAlias: async () => {
  74. return {} as CollectionFullObject;
  75. },
  76. dropAlias: async () => {
  77. return {} as CollectionFullObject;
  78. },
  79. setCollectionProperty: async () => {
  80. return {} as CollectionFullObject;
  81. },
  82. ui: {
  83. tree: {
  84. width: DEFAULT_TREE_WIDTH,
  85. },
  86. },
  87. setUIPref: () => {},
  88. });
  89. const { Provider } = dataContext;
  90. export const DataProvider = (props: { children: React.ReactNode }) => {
  91. // auth context
  92. const { authReq, isAuth, clientId, logout, setAuthReq } =
  93. useContext(authContext);
  94. // UI preferences
  95. const [ui, setUI] = useState({
  96. tree: {
  97. width: DEFAULT_TREE_WIDTH,
  98. },
  99. });
  100. // local data state
  101. const [collections, setCollections] = useState<CollectionObject[]>([]);
  102. const [connected, setConnected] = useState(false);
  103. const [loading, setLoading] = useState(true);
  104. const [loadingDatabases, setLoadingDatabases] = useState(true);
  105. const [database, setDatabase] = useState<string>(authReq.database);
  106. const [databases, setDatabases] = useState<DatabaseObject[]>([]);
  107. // socket ref
  108. const socket = useRef<Socket | null>(null);
  109. // Use a ref to track concurrent requests
  110. const requestIdRef = useRef(0);
  111. // collection state test
  112. const detectLoadingIndexing = useCallback(
  113. (collections: CollectionObject[]) => {
  114. const LoadingOrBuildingCollections = collections.filter(v => {
  115. const isLoading = checkLoading(v);
  116. const isBuildingIndex = checkIndexing(v);
  117. return isLoading || isBuildingIndex;
  118. });
  119. // trigger cron if it has to
  120. if (LoadingOrBuildingCollections.length > 0) {
  121. MilvusService.triggerCron({
  122. name: WS_EVENTS.COLLECTION_UPDATE,
  123. type: WS_EVENTS_TYPE.START,
  124. payload: {
  125. database,
  126. collections: LoadingOrBuildingCollections.map(
  127. c => c.collection_name
  128. ),
  129. },
  130. });
  131. }
  132. },
  133. [database]
  134. );
  135. // Websocket Callback: update single collection
  136. const updateCollections = useCallback(
  137. (props: { collections: CollectionFullObject[]; database?: string }) => {
  138. const { collections, database: remote } = props;
  139. if (
  140. remote !== database &&
  141. database !== undefined &&
  142. remote !== undefined
  143. ) {
  144. // console.log('database not matched', remote, database);
  145. return;
  146. }
  147. // check state to see if it is loading or building index, if so, start server cron job
  148. detectLoadingIndexing(collections);
  149. // update single collection
  150. setCollections(prev => {
  151. // update exist collection
  152. const newCollections = prev.map(v => {
  153. const collectionToUpdate = collections.find(c => c.id === v.id);
  154. if (collectionToUpdate) {
  155. return collectionToUpdate;
  156. }
  157. return v;
  158. });
  159. return newCollections;
  160. });
  161. },
  162. [database]
  163. );
  164. // API: fetch databases
  165. const fetchDatabases = async (updateLoading?: boolean) => {
  166. try {
  167. updateLoading && setLoadingDatabases(true);
  168. const newDatabases = await DatabaseService.listDatabases();
  169. // if no database, logout
  170. if (newDatabases.length === 0) {
  171. logout();
  172. }
  173. setDatabases(newDatabases);
  174. return newDatabases;
  175. } finally {
  176. updateLoading && setLoadingDatabases(false);
  177. }
  178. };
  179. // API: create database
  180. const createDatabase = async (params: { db_name: string }) => {
  181. await DatabaseService.createDatabase(params);
  182. await fetchDatabases();
  183. };
  184. // API: delete database
  185. const dropDatabase = async (params: { db_name: string }) => {
  186. await DatabaseService.dropDatabase(params);
  187. const newDatabases = await fetchDatabases();
  188. setDatabase(newDatabases[0].name);
  189. };
  190. // API:fetch collections
  191. const fetchCollections = async () => {
  192. const currentRequestId = ++requestIdRef.current;
  193. try {
  194. // set loading true
  195. setLoading(true);
  196. // set collections
  197. setCollections([]);
  198. // fetch collections
  199. const res = await CollectionService.getCollections();
  200. // Only process the response if this is the latest request
  201. if (currentRequestId === requestIdRef.current) {
  202. // check state
  203. detectLoadingIndexing(res);
  204. // set collections
  205. setCollections(res);
  206. // set loading false
  207. setLoading(false);
  208. }
  209. } catch (error) {
  210. if (currentRequestId === requestIdRef.current) {
  211. setLoading(false);
  212. }
  213. throw error;
  214. }
  215. };
  216. // API: fetch single collection
  217. const fetchCollection = async (name: string) => {
  218. // fetch collections
  219. const res = await CollectionService.getCollection(name);
  220. // update collection
  221. updateCollections({ collections: [res] });
  222. return res;
  223. };
  224. // API: create collection
  225. const createCollection = async (data: any) => {
  226. // create collection
  227. const newCollection = await CollectionService.createCollection(data);
  228. // combine new collection with old collections
  229. // sort state by createdTime.
  230. const newCollections = collections.concat(newCollection).sort((a, b) => {
  231. if (a.loadedPercentage === b.loadedPercentage && a.schema && b.schema) {
  232. if (a.schema.hasVectorIndex === b.schema.hasVectorIndex) {
  233. return b.createdTime - a.createdTime;
  234. }
  235. return a.schema.hasVectorIndex ? -1 : 1;
  236. }
  237. return (b.loadedPercentage || 0) - (a.loadedPercentage || 0);
  238. });
  239. // update collection
  240. setCollections(newCollections);
  241. return newCollection;
  242. };
  243. // API: load collection
  244. const loadCollection = async (name: string, param?: any) => {
  245. // load collection
  246. await CollectionService.loadCollection(name, param);
  247. // find the collection in the collections
  248. const collection = collections.find(
  249. v => v.collection_name === name
  250. ) as CollectionFullObject;
  251. // update collection infomation
  252. if (collection) {
  253. collection.loadedPercentage = 0;
  254. collection.loaded = false;
  255. collection.status = LOADING_STATE.LOADING;
  256. }
  257. // update collection, and trigger cron job
  258. updateCollections({ collections: [collection] });
  259. };
  260. // API: release collection
  261. const releaseCollection = async (name: string) => {
  262. // release collection
  263. await CollectionService.releaseCollection(name);
  264. };
  265. // API: rename collection
  266. const renameCollection = async (name: string, newName: string) => {
  267. // rename collection
  268. const newCollection = await CollectionService.renameCollection(name, {
  269. new_collection_name: newName,
  270. });
  271. updateCollections({ collections: [newCollection] });
  272. return newCollection;
  273. };
  274. // API: duplicate collection
  275. const duplicateCollection = async (name: string, newName: string) => {
  276. // duplicate collection
  277. const newCollection = await CollectionService.duplicateCollection(name, {
  278. new_collection_name: newName,
  279. });
  280. // inset collection to state
  281. setCollections(prev => [...prev, newCollection]);
  282. return newCollection;
  283. };
  284. // API: drop collection
  285. const dropCollection = async (name: string) => {
  286. // drop collection
  287. const dropped = await CollectionService.dropCollection(name);
  288. if (dropped.data.error_code === 'Success') {
  289. // remove collection from state
  290. setCollections(prev => prev.filter(v => v.collection_name !== name));
  291. }
  292. return dropped.data;
  293. };
  294. // API: create index
  295. const createIndex = async (param: IndexCreateParam) => {
  296. // create index
  297. const newCollection = await CollectionService.createIndex(param);
  298. // update collection
  299. updateCollections({ collections: [newCollection] });
  300. return newCollection;
  301. };
  302. // API: drop index
  303. const dropIndex = async (params: IndexManageParam) => {
  304. // drop index
  305. const { data } = await CollectionService.dropIndex(params);
  306. // update collection
  307. updateCollections({ collections: [data] });
  308. return data;
  309. };
  310. // API: create alias
  311. const createAlias = async (collectionName: string, alias: string) => {
  312. // create alias
  313. const newCollection = await CollectionService.createAlias(collectionName, {
  314. alias,
  315. });
  316. // update collection
  317. updateCollections({ collections: [newCollection] });
  318. return newCollection;
  319. };
  320. // API: drop alias
  321. const dropAlias = async (collectionName: string, alias: string) => {
  322. // drop alias
  323. const { data } = await CollectionService.dropAlias(collectionName, {
  324. alias,
  325. });
  326. // update collection
  327. updateCollections({ collections: [data] });
  328. return data;
  329. };
  330. // API: set property
  331. const setCollectionProperty = async (
  332. collectionName: string,
  333. key: string,
  334. value: any
  335. ) => {
  336. // set property
  337. const newCollection = await CollectionService.setProperty(collectionName, {
  338. [key]: value,
  339. });
  340. // update existing collection
  341. updateCollections({ collections: [newCollection] });
  342. return newCollection;
  343. };
  344. // set UI preferences
  345. const setUIPref = (pref: DataContextType['ui']) => {
  346. setUI(pref);
  347. localStorage.setItem(ATTU_UI_TREE_WIDTH, String(pref.tree.width));
  348. };
  349. // load UI preferences
  350. useEffect(() => {
  351. const storedWidth = Number(localStorage.getItem(ATTU_UI_TREE_WIDTH));
  352. if (storedWidth) {
  353. setUI(prevUI => ({
  354. ...prevUI,
  355. tree: {
  356. ...prevUI.tree,
  357. width: storedWidth,
  358. },
  359. }));
  360. }
  361. }, []);
  362. useEffect(() => {
  363. const clear = () => {
  364. // clear collections
  365. setCollections([]);
  366. // clear database
  367. setDatabases([]);
  368. // set connected to false
  369. setConnected(false);
  370. // remove all listeners when component unmount
  371. socket.current?.offAny();
  372. socket.current?.disconnect();
  373. };
  374. if (isAuth) {
  375. // update database get from auth
  376. setDatabase(authReq.database);
  377. const extraHeaders = {
  378. 'milvus-client-id': clientId,
  379. };
  380. const ioParams = { extraHeaders, query: extraHeaders };
  381. socket.current = isElectron ? io(url as string, ioParams) : io(ioParams);
  382. socket.current.on('connect', async () => {
  383. // console.info('--- ws connected ---', clientId);
  384. // fetch db
  385. await fetchDatabases(true);
  386. // set connected to trues
  387. setConnected(true);
  388. });
  389. // handle disconnect
  390. socket.current.on('disconnect', () => {
  391. // Set connected to false
  392. setConnected(false);
  393. });
  394. // handle error
  395. socket.current.on('error', error => {
  396. socket.current?.disconnect();
  397. });
  398. } else {
  399. clear();
  400. }
  401. return () => {
  402. clear();
  403. };
  404. }, [isAuth]);
  405. useEffect(() => {
  406. if (connected) {
  407. // clear data
  408. setCollections([]);
  409. // remove all listeners
  410. socket.current?.off(WS_EVENTS.COLLECTION_UPDATE, updateCollections);
  411. // listen to backend collection event
  412. socket.current?.on(WS_EVENTS.COLLECTION_UPDATE, updateCollections);
  413. // fetch db
  414. fetchCollections();
  415. }
  416. return () => {
  417. socket.current?.off(WS_EVENTS.COLLECTION_UPDATE, updateCollections);
  418. };
  419. }, [updateCollections, connected]);
  420. useEffect(() => {
  421. setAuthReq({ ...authReq, database });
  422. }, [database]);
  423. return (
  424. <Provider
  425. value={{
  426. loading,
  427. loadingDatabases,
  428. collections,
  429. setCollections,
  430. database,
  431. databases,
  432. setDatabase,
  433. setDatabaseList: setDatabases,
  434. createDatabase,
  435. dropDatabase,
  436. fetchDatabases,
  437. fetchCollections,
  438. fetchCollection,
  439. createCollection,
  440. loadCollection,
  441. releaseCollection,
  442. renameCollection,
  443. duplicateCollection,
  444. dropCollection,
  445. createIndex,
  446. dropIndex,
  447. createAlias,
  448. dropAlias,
  449. setCollectionProperty,
  450. ui,
  451. setUIPref,
  452. }}
  453. >
  454. {props.children}
  455. </Provider>
  456. );
  457. };