AuthForm.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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. // update title
  196. document.title = 'Attu';
  197. }, [authReq.address, authReq.username, authReq.password]);
  198. return (
  199. <form onSubmit={handleConnect}>
  200. <section className={classes.wrapper}>
  201. <div className={classes.titleWrapper}>
  202. <Typography variant="h4" component="h4">
  203. {attuTrans.connectTitle}
  204. <CustomToolTip title={attuTrans.connectionTip}>
  205. <Icons.info />
  206. </CustomToolTip>
  207. </Typography>
  208. </div>
  209. {/* address */}
  210. <CustomInput
  211. type="text"
  212. textConfig={{
  213. label: attuTrans.address,
  214. key: 'address',
  215. onChange: (val: string) =>
  216. handleInputChange('address', String(val)),
  217. variant: 'filled',
  218. className: classes.input,
  219. placeholder: attuTrans.address,
  220. fullWidth: true,
  221. InputProps: {
  222. endAdornment: (
  223. <CustomIconButton
  224. className={classes.menuBtn}
  225. onClick={handleMenuClick}
  226. >
  227. <Icons.link />
  228. </CustomIconButton>
  229. ),
  230. },
  231. validations: [
  232. {
  233. rule: 'require',
  234. errorText: warningTrans('required', {
  235. name: attuTrans.address,
  236. }),
  237. },
  238. ],
  239. value: authReq.address,
  240. }}
  241. checkValid={checkIsValid}
  242. validInfo={validation}
  243. key={attuTrans.address}
  244. />
  245. {/* db */}
  246. <CustomInput
  247. type="text"
  248. textConfig={{
  249. label: `Milvus ${dbTrans('database')} ${attuTrans.optional}`,
  250. key: 'database',
  251. onChange: (value: string) => handleInputChange('database', value),
  252. variant: 'filled',
  253. className: classes.input,
  254. placeholder: dbTrans('database'),
  255. fullWidth: true,
  256. value: authReq.database,
  257. }}
  258. checkValid={checkIsValid}
  259. validInfo={validation}
  260. key={attuTrans.database}
  261. />
  262. {/* toggle auth */}
  263. <div className={classes.toggle}>
  264. <CustomRadio
  265. checked={withPass}
  266. label={attuTrans.authentication}
  267. handleChange={handleEnableAuth}
  268. />
  269. </div>
  270. {/* token */}
  271. {withPass && (
  272. <>
  273. <CustomInput
  274. type="text"
  275. textConfig={{
  276. label: `${attuTrans.token} ${attuTrans.optional} `,
  277. key: 'token',
  278. onChange: (val: string) => handleInputChange('token', val),
  279. variant: 'filled',
  280. className: classes.input,
  281. placeholder: attuTrans.token,
  282. fullWidth: true,
  283. value: authReq.token,
  284. }}
  285. checkValid={checkIsValid}
  286. validInfo={validation}
  287. key={attuTrans.token}
  288. />
  289. {/* user */}
  290. <CustomInput
  291. type="text"
  292. textConfig={{
  293. label: `${attuTrans.username} ${attuTrans.optional}`,
  294. key: 'username',
  295. onChange: (value: string) =>
  296. handleInputChange('username', value),
  297. variant: 'filled',
  298. className: classes.input,
  299. placeholder: attuTrans.username,
  300. fullWidth: true,
  301. value: authReq.username,
  302. }}
  303. checkValid={checkIsValid}
  304. validInfo={validation}
  305. key={attuTrans.username}
  306. />
  307. {/* pass */}
  308. <CustomInput
  309. type="text"
  310. textConfig={{
  311. label: `${attuTrans.password} ${attuTrans.optional}`,
  312. key: 'password',
  313. onChange: (value: string) =>
  314. handleInputChange('password', value),
  315. variant: 'filled',
  316. className: classes.input,
  317. placeholder: attuTrans.password,
  318. fullWidth: true,
  319. type: 'password',
  320. value: authReq.password,
  321. }}
  322. checkValid={checkIsValid}
  323. validInfo={validation}
  324. key={attuTrans.password}
  325. />
  326. </>
  327. )}
  328. <CustomButton type="submit" variant="contained" disabled={btnDisabled}>
  329. {btnTrans(isConnecting ? 'connecting' : 'connect')}
  330. </CustomButton>
  331. </section>
  332. <Menu
  333. anchorEl={anchorEl}
  334. keepMounted
  335. className={classes.menu}
  336. anchorOrigin={{
  337. vertical: 'bottom',
  338. horizontal: 'right',
  339. }}
  340. transformOrigin={{
  341. vertical: 'top',
  342. horizontal: 'right',
  343. }}
  344. open={Boolean(anchorEl)}
  345. onClose={handleMenuClose}
  346. >
  347. {connections.map((connection, index) => (
  348. <li
  349. key={index}
  350. className={classes.connection}
  351. onClick={() => {
  352. handleClickOnHisotry(connection);
  353. }}
  354. >
  355. <div className="address">
  356. <Icons.link className="icon"></Icons.link>
  357. <div className="text">
  358. {connection.address}/{connection.database}
  359. </div>
  360. </div>
  361. <div className="time">
  362. {connection.time !== -1
  363. ? new Date(connection.time).toLocaleString()
  364. : '--'}
  365. </div>
  366. <div>
  367. {connection.time !== -1 && (
  368. <CustomIconButton
  369. className="deleteIconBtn"
  370. onClick={e => {
  371. e.stopPropagation();
  372. handleDeleteConnection(connection);
  373. }}
  374. >
  375. <Icons.cross></Icons.cross>
  376. </CustomIconButton>
  377. )}
  378. </div>
  379. </li>
  380. ))}
  381. </Menu>
  382. </form>
  383. );
  384. };