AuthForm.tsx 12 KB

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