Data.tsx 13 KB

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