AuthForm.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import React, { useContext, useEffect, useMemo, useState } from 'react';
  2. import { Typography, Menu } from '@mui/material';
  3. import { useTranslation } from 'react-i18next';
  4. import CustomButton from '@/components/customButton/CustomButton';
  5. import CustomInput from '@/components/customInput/CustomInput';
  6. import { useFormValidation } from '@/hooks';
  7. import { formatForm } from '@/utils';
  8. import { useNavigate } from 'react-router-dom';
  9. import { rootContext, authContext, dataContext } from '@/context';
  10. import {
  11. MILVUS_CLIENT_ID,
  12. ATTU_AUTH_HISTORY,
  13. MILVUS_DATABASE,
  14. MILVUS_URL,
  15. } from '@/consts';
  16. import { CustomRadio } from '@/components/customRadio/CustomRadio';
  17. import Icons from '@/components/icons/Icons';
  18. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  19. import CustomIconButton from '@/components/customButton/CustomIconButton';
  20. import { useStyles } from './style';
  21. import { AuthReq } from '@server/types';
  22. type Connection = AuthReq & {
  23. time: number;
  24. };
  25. const DEFAULT_CONNECTION = {
  26. address: MILVUS_URL || '127.0.0.1:19530',
  27. database: MILVUS_DATABASE,
  28. token: '',
  29. username: '',
  30. password: '',
  31. time: -1,
  32. };
  33. export const AuthForm = () => {
  34. // styles
  35. const classes = useStyles();
  36. // context
  37. const { openSnackBar } = useContext(rootContext);
  38. const { authReq, setAuthReq, login } = useContext(authContext);
  39. const { setDatabase } = useContext(dataContext);
  40. // i18n
  41. const { t: commonTrans } = useTranslation();
  42. const attuTrans = commonTrans('attu');
  43. const { t: btnTrans } = useTranslation('btn');
  44. const { t: warningTrans } = useTranslation('warning');
  45. const { t: successTrans } = useTranslation('success');
  46. const { t: dbTrans } = useTranslation('database');
  47. // hooks
  48. const navigate = useNavigate();
  49. // UI states
  50. const [withPass, setWithPass] = useState(false);
  51. const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  52. const [connections, setConnections] = useState<Connection[]>([]);
  53. const [isConnecting, setIsConnecting] = useState(false);
  54. // form validation
  55. const checkedForm = useMemo(() => {
  56. return formatForm(authReq);
  57. }, [authReq]);
  58. const { validation, checkIsValid, resetValidation } =
  59. useFormValidation(checkedForm);
  60. // UI handlers
  61. // handle input change
  62. const handleInputChange = (
  63. key: 'address' | 'username' | 'password' | 'database' | 'token',
  64. value: string | boolean
  65. ) => {
  66. // set database to default if empty
  67. if (key === 'database' && value === '') {
  68. value = MILVUS_DATABASE;
  69. }
  70. setAuthReq(v => ({ ...v, [key]: value }));
  71. };
  72. // handle menu clicked
  73. const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  74. setAnchorEl(event.currentTarget);
  75. };
  76. // handle menu close
  77. const handleMenuClose = () => {
  78. setAnchorEl(null);
  79. };
  80. // handle auth toggle
  81. const handleEnableAuth = (val: boolean) => {
  82. setWithPass(val);
  83. };
  84. // handle connect
  85. const handleConnect = async (event: React.FormEvent) => {
  86. event.preventDefault();
  87. // set connecting
  88. setIsConnecting(true);
  89. try {
  90. // login
  91. const result = await login(authReq);
  92. // set database
  93. setDatabase(authReq.database);
  94. // success message
  95. openSnackBar(successTrans('connect'));
  96. // save clientId to local storage
  97. window.localStorage.setItem(MILVUS_CLIENT_ID, result.clientId);
  98. // get connection history
  99. const history = JSON.parse(
  100. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  101. );
  102. // add new connection to history, filter out the same connection
  103. const newHistory = [
  104. ...history.filter(
  105. (item: any) =>
  106. item.address !== authReq.address ||
  107. item.database !== authReq.database
  108. ),
  109. {
  110. address: authReq.address,
  111. database: authReq.database,
  112. username: authReq.username,
  113. password: authReq.password,
  114. token: authReq.token,
  115. time: Date.now(),
  116. },
  117. ];
  118. // if the count of history connections are more than 16, remove the first one, but it should keep the default one
  119. if (newHistory.length > 16) {
  120. newHistory.shift();
  121. }
  122. // save to local storage
  123. window.localStorage.setItem(
  124. ATTU_AUTH_HISTORY,
  125. JSON.stringify(newHistory)
  126. );
  127. // set title
  128. document.title = authReq.address ? `${authReq.address} - Attu` : 'Attu';
  129. // redirect to homepage
  130. navigate('/');
  131. } catch (error: any) {
  132. // if not authorized, show auth inputs
  133. if (error.response.data.message.includes('UNAUTHENTICATED')) {
  134. handleEnableAuth(true);
  135. }
  136. } finally {
  137. setIsConnecting(false);
  138. }
  139. };
  140. // connect history clicked
  141. const handleClickOnHisotry = (connection: Connection) => {
  142. // set auth request
  143. setAuthReq(connection);
  144. // close menu
  145. handleMenuClose();
  146. };
  147. const handleDeleteConnection = (connection: Connection) => {
  148. const history = JSON.parse(
  149. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  150. ) as Connection[];
  151. const newHistory = history.filter(
  152. item =>
  153. item.address !== connection.address ||
  154. item.database !== connection.database
  155. );
  156. if (newHistory.length === 0) {
  157. newHistory.push(DEFAULT_CONNECTION);
  158. }
  159. // save to local storage
  160. window.localStorage.setItem(ATTU_AUTH_HISTORY, JSON.stringify(newHistory));
  161. // sort by time
  162. newHistory.sort((a, b) => {
  163. return new Date(b.time).getTime() - new Date(a.time).getTime();
  164. });
  165. setConnections(newHistory);
  166. };
  167. // is button should be disabled
  168. const btnDisabled = authReq.address.trim().length === 0 || isConnecting;
  169. // load connection from local storage
  170. useEffect(() => {
  171. const connections: Connection[] = JSON.parse(
  172. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  173. );
  174. if (connections.length === 0) {
  175. connections.push(DEFAULT_CONNECTION);
  176. }
  177. // sort by time
  178. connections.sort((a, b) => {
  179. return new Date(b.time).getTime() - new Date(a.time).getTime();
  180. });
  181. setConnections(connections);
  182. }, []);
  183. // UI effect
  184. useEffect(() => {
  185. // if address contains zilliz, or username or password is not empty
  186. // set withpass to true
  187. const withPass =
  188. (authReq.address.length > 0 && authReq.address.includes('zilliz')) ||
  189. authReq.username.length > 0 ||
  190. authReq.password.length > 0;
  191. // set with pass
  192. setWithPass(withPass);
  193. // reset form
  194. resetValidation(formatForm(authReq));
  195. }, [authReq.address, authReq.username, authReq.password]);
  196. return (
  197. <form onSubmit={handleConnect}>
  198. <section className={classes.wrapper}>
  199. <div className={classes.titleWrapper}>
  200. <Typography variant="h4" component="h4">
  201. {attuTrans.connectTitle}
  202. <CustomToolTip title={attuTrans.connectionTip}>
  203. <Icons.info />
  204. </CustomToolTip>
  205. </Typography>
  206. </div>
  207. {/* address */}
  208. <CustomInput
  209. type="text"
  210. textConfig={{
  211. label: attuTrans.address,
  212. key: 'address',
  213. onChange: (val: string) =>
  214. handleInputChange('address', String(val)),
  215. variant: 'filled',
  216. className: classes.input,
  217. placeholder: attuTrans.address,
  218. fullWidth: true,
  219. InputProps: {
  220. endAdornment: (
  221. <CustomIconButton
  222. className={classes.menuBtn}
  223. onClick={handleMenuClick}
  224. >
  225. <Icons.link />
  226. </CustomIconButton>
  227. ),
  228. },
  229. validations: [
  230. {
  231. rule: 'require',
  232. errorText: warningTrans('required', {
  233. name: attuTrans.address,
  234. }),
  235. },
  236. ],
  237. value: authReq.address,
  238. }}
  239. checkValid={checkIsValid}
  240. validInfo={validation}
  241. key={attuTrans.address}
  242. />
  243. {/* db */}
  244. <CustomInput
  245. type="text"
  246. textConfig={{
  247. label: `Milvus ${dbTrans('database')} ${attuTrans.optional}`,
  248. key: 'database',
  249. onChange: (value: string) => handleInputChange('database', value),
  250. variant: 'filled',
  251. className: classes.input,
  252. placeholder: dbTrans('database'),
  253. fullWidth: true,
  254. value: authReq.database,
  255. }}
  256. checkValid={checkIsValid}
  257. validInfo={validation}
  258. key={attuTrans.database}
  259. />
  260. {/* toggle auth */}
  261. <div className={classes.toggle}>
  262. <CustomRadio
  263. checked={withPass}
  264. label={attuTrans.authentication}
  265. handleChange={handleEnableAuth}
  266. />
  267. </div>
  268. {/* token */}
  269. {withPass && (
  270. <>
  271. <CustomInput
  272. type="text"
  273. textConfig={{
  274. label: `${attuTrans.token} ${attuTrans.optional} `,
  275. key: 'token',
  276. onChange: (val: string) => handleInputChange('token', val),
  277. variant: 'filled',
  278. className: classes.input,
  279. placeholder: attuTrans.token,
  280. fullWidth: true,
  281. value: authReq.token,
  282. }}
  283. checkValid={checkIsValid}
  284. validInfo={validation}
  285. key={attuTrans.token}
  286. />
  287. {/* user */}
  288. <CustomInput
  289. type="text"
  290. textConfig={{
  291. label: `${attuTrans.username} ${attuTrans.optional}`,
  292. key: 'username',
  293. onChange: (value: string) =>
  294. handleInputChange('username', value),
  295. variant: 'filled',
  296. className: classes.input,
  297. placeholder: attuTrans.username,
  298. fullWidth: true,
  299. value: authReq.username,
  300. }}
  301. checkValid={checkIsValid}
  302. validInfo={validation}
  303. key={attuTrans.username}
  304. />
  305. {/* pass */}
  306. <CustomInput
  307. type="text"
  308. textConfig={{
  309. label: `${attuTrans.password} ${attuTrans.optional}`,
  310. key: 'password',
  311. onChange: (value: string) =>
  312. handleInputChange('password', value),
  313. variant: 'filled',
  314. className: classes.input,
  315. placeholder: attuTrans.password,
  316. fullWidth: true,
  317. type: 'password',
  318. value: authReq.password,
  319. }}
  320. checkValid={checkIsValid}
  321. validInfo={validation}
  322. key={attuTrans.password}
  323. />
  324. </>
  325. )}
  326. <CustomButton type="submit" variant="contained" disabled={btnDisabled}>
  327. {btnTrans(isConnecting ? 'connecting' : 'connect')}
  328. </CustomButton>
  329. </section>
  330. <Menu
  331. anchorEl={anchorEl}
  332. keepMounted
  333. className={classes.menu}
  334. anchorOrigin={{
  335. vertical: 'bottom',
  336. horizontal: 'right',
  337. }}
  338. transformOrigin={{
  339. vertical: 'top',
  340. horizontal: 'right',
  341. }}
  342. open={Boolean(anchorEl)}
  343. onClose={handleMenuClose}
  344. >
  345. {connections.map((connection, index) => (
  346. <li
  347. key={index}
  348. className={classes.connection}
  349. onClick={() => {
  350. handleClickOnHisotry(connection);
  351. }}
  352. >
  353. <div className="address">
  354. <Icons.link className="icon"></Icons.link>
  355. <div className="text">
  356. {connection.address}/{connection.database}
  357. </div>
  358. </div>
  359. <div className="time">
  360. {connection.time !== -1
  361. ? new Date(connection.time).toLocaleString()
  362. : '--'}
  363. </div>
  364. <div>
  365. {connection.time !== -1 && (
  366. <CustomIconButton
  367. className="deleteIconBtn"
  368. onClick={e => {
  369. e.stopPropagation();
  370. handleDeleteConnection(connection);
  371. }}
  372. >
  373. <Icons.cross></Icons.cross>
  374. </CustomIconButton>
  375. )}
  376. </div>
  377. </li>
  378. ))}
  379. </Menu>
  380. </form>
  381. );
  382. };