AuthForm.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. import React, { useContext, useEffect, useMemo, useState } from 'react';
  2. import Typography from '@mui/material/Typography';
  3. import Menu from '@mui/material/Menu';
  4. import Checkbox from '@mui/material/Checkbox';
  5. import { useTranslation } from 'react-i18next';
  6. import CustomButton from '@/components/customButton/CustomButton';
  7. import CustomInput from '@/components/customInput/CustomInput';
  8. import { useFormValidation } from '@/hooks';
  9. import { formatForm } from '@/utils';
  10. import { useNavigate } from 'react-router-dom';
  11. import { rootContext, authContext, dataContext } from '@/context';
  12. import { ATTU_AUTH_HISTORY, MILVUS_DATABASE, MILVUS_URL } from '@/consts';
  13. import { CustomRadio } from '@/components/customRadio/CustomRadio';
  14. import Icons from '@/components/icons/Icons';
  15. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  16. import CustomIconButton from '@/components/customButton/CustomIconButton';
  17. import type { AuthReq } from '@server/types';
  18. import FormControlLabel from '@mui/material/FormControlLabel';
  19. import Box from '@mui/material/Box';
  20. import type { Theme } from '@mui/material';
  21. // Add Connection type definition back
  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. ssl: false,
  32. checkHealth: true,
  33. time: -1,
  34. clientId: '',
  35. };
  36. export const AuthForm = () => {
  37. // styles
  38. // const classes = useStyles(); // Removed useStyles
  39. // context
  40. const { openSnackBar } = useContext(rootContext);
  41. const { authReq, setAuthReq, login } = useContext(authContext);
  42. const { setDatabase } = useContext(dataContext);
  43. // i18n
  44. const { t: commonTrans } = useTranslation();
  45. const { t: btnTrans } = useTranslation('btn');
  46. const { t: warningTrans } = useTranslation('warning');
  47. const { t: successTrans } = useTranslation('success');
  48. const { t: dbTrans } = useTranslation('database');
  49. // hooks
  50. const navigate = useNavigate();
  51. // UI states
  52. const [withPass, setWithPass] = useState(false);
  53. const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  54. const [connections, setConnections] = useState<Connection[]>([]);
  55. const [isConnecting, setIsConnecting] = useState(false);
  56. // form validation
  57. const checkedForm = useMemo(() => {
  58. return formatForm(authReq);
  59. }, [authReq]);
  60. const { validation, checkIsValid, resetValidation } =
  61. useFormValidation(checkedForm);
  62. // UI handlers
  63. // handle input change
  64. const handleInputChange = (
  65. key:
  66. | 'address'
  67. | 'username'
  68. | 'password'
  69. | 'ssl'
  70. | 'database'
  71. | 'token'
  72. | 'checkHealth',
  73. value: string | boolean
  74. ) => {
  75. // set database to default if empty
  76. // if (key === 'database' && value === '') {
  77. // value = MILVUS_DATABASE;
  78. // }
  79. setAuthReq(v => ({ ...v, [key]: value }));
  80. };
  81. // handle menu clicked
  82. const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  83. setAnchorEl(event.currentTarget);
  84. };
  85. // handle menu close
  86. const handleMenuClose = () => {
  87. setAnchorEl(null);
  88. };
  89. // handle auth toggle
  90. const handleEnableAuth = (val: boolean) => {
  91. setWithPass(val);
  92. };
  93. // handle connect
  94. const handleConnect = async (event: React.FormEvent) => {
  95. event.preventDefault();
  96. // set connecting
  97. setIsConnecting(true);
  98. try {
  99. // login
  100. const loginParams = { ...authReq };
  101. if (!withPass) {
  102. loginParams.username = '';
  103. loginParams.password = '';
  104. loginParams.token = '';
  105. }
  106. await login(loginParams);
  107. // set database
  108. setDatabase(authReq.database);
  109. // success message
  110. openSnackBar(successTrans('connect'));
  111. // get connection history
  112. const history = JSON.parse(
  113. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  114. );
  115. // add new connection to history, filter out the same connection
  116. const newHistory = [
  117. ...history.filter(
  118. (item: any) =>
  119. item.address !== authReq.address ||
  120. item.database !== authReq.database
  121. ),
  122. {
  123. address: authReq.address,
  124. database: authReq.database,
  125. username: authReq.username,
  126. password: authReq.password,
  127. token: authReq.token,
  128. time: Date.now(),
  129. checkHealth: authReq.checkHealth,
  130. },
  131. ];
  132. // if the count of history connections are more than 16, remove the first one, but it should keep the default one
  133. if (newHistory.length > 16) {
  134. newHistory.shift();
  135. }
  136. // save to local storage
  137. window.localStorage.setItem(
  138. ATTU_AUTH_HISTORY,
  139. JSON.stringify(newHistory)
  140. );
  141. // set title
  142. document.title = authReq.address ? `${authReq.address} - Attu` : 'Attu';
  143. // redirect to homepage
  144. navigate('/');
  145. } catch (error: any) {
  146. // if not authorized, show auth inputs
  147. if (error.response.data.message.includes('UNAUTHENTICATED')) {
  148. handleEnableAuth(true);
  149. }
  150. } finally {
  151. setIsConnecting(false);
  152. }
  153. };
  154. // connect history clicked
  155. const handleClickOnHisotry = (connection: Connection) => {
  156. // set auth request
  157. setAuthReq(connection);
  158. // close menu
  159. handleMenuClose();
  160. };
  161. const handleDeleteConnection = (connection: Connection) => {
  162. const history = JSON.parse(
  163. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  164. ) as Connection[];
  165. const newHistory = history.filter(
  166. item =>
  167. item.address !== connection.address ||
  168. item.database !== connection.database
  169. );
  170. if (newHistory.length === 0) {
  171. newHistory.push(DEFAULT_CONNECTION);
  172. }
  173. // save to local storage
  174. window.localStorage.setItem(ATTU_AUTH_HISTORY, JSON.stringify(newHistory));
  175. // sort by time
  176. newHistory.sort((a, b) => {
  177. return new Date(b.time).getTime() - new Date(a.time).getTime();
  178. });
  179. setConnections(newHistory);
  180. };
  181. // is button should be disabled
  182. const btnDisabled = authReq.address.trim().length === 0 || isConnecting;
  183. // load connection from local storage
  184. useEffect(() => {
  185. const connections: Connection[] = JSON.parse(
  186. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  187. );
  188. if (connections.length === 0) {
  189. connections.push(DEFAULT_CONNECTION);
  190. }
  191. // sort by time
  192. connections.sort((a, b) => {
  193. return new Date(b.time).getTime() - new Date(a.time).getTime();
  194. });
  195. setConnections(connections);
  196. }, []);
  197. // UI effect
  198. useEffect(() => {
  199. // if address contains zilliz, or username or password is not empty
  200. // set withpass to true
  201. const withPass =
  202. (authReq.address.length > 0 && authReq.address.includes('zilliz')) ||
  203. authReq.username.length > 0 ||
  204. authReq.password.length > 0;
  205. // set with pass
  206. setWithPass(withPass);
  207. // reset form
  208. resetValidation(formatForm(authReq));
  209. }, [authReq.address, authReq.username, authReq.password]);
  210. return (
  211. <form onSubmit={handleConnect}>
  212. <Box
  213. sx={{
  214. display: 'flex',
  215. flexDirection: 'column',
  216. padding: (theme: Theme) => theme.spacing(0, 3),
  217. position: 'relative',
  218. }}
  219. >
  220. <Box
  221. sx={{
  222. textAlign: 'left',
  223. alignSelf: 'flex-start',
  224. padding: (theme: Theme) => theme.spacing(3, 0),
  225. '& svg': {
  226. fontSize: 15,
  227. marginLeft: (theme: Theme) => theme.spacing(0.5),
  228. },
  229. }}
  230. >
  231. <Typography variant="h4" component="h4">
  232. {commonTrans('attu.connectTitle')}
  233. <CustomToolTip title={commonTrans('attu.connectionTip')}>
  234. <Icons.info />
  235. </CustomToolTip>
  236. </Typography>
  237. </Box>
  238. {/* address */}
  239. <CustomInput
  240. type="text"
  241. textConfig={{
  242. label: commonTrans('attu.address'),
  243. key: 'address',
  244. onChange: (val: string) =>
  245. handleInputChange('address', String(val)),
  246. variant: 'filled',
  247. sx: {
  248. margin: (theme: Theme) => theme.spacing(0.5, 0, 0),
  249. '& .MuiFilledInput-adornedEnd': {
  250. paddingRight: 0,
  251. },
  252. },
  253. placeholder: commonTrans('attu.address'),
  254. fullWidth: true,
  255. InputProps: {
  256. endAdornment: (
  257. <CustomIconButton
  258. sx={{
  259. display: 'flex',
  260. paddingLeft: 1,
  261. paddingRight: 1,
  262. fontSize: 14,
  263. '& button': {
  264. width: 36,
  265. height: 36,
  266. },
  267. }}
  268. onClick={handleMenuClick}
  269. >
  270. <Icons.link />
  271. </CustomIconButton>
  272. ),
  273. },
  274. validations: [
  275. {
  276. rule: 'require',
  277. errorText: warningTrans('required', {
  278. name: commonTrans('attu.address'),
  279. }),
  280. },
  281. ],
  282. value: authReq.address,
  283. }}
  284. checkValid={checkIsValid}
  285. validInfo={validation}
  286. key={commonTrans('attu.address')}
  287. />
  288. {/* db */}
  289. <CustomInput
  290. type="text"
  291. textConfig={{
  292. label: `Milvus ${dbTrans('database')} ${commonTrans('attu.optional')}`,
  293. key: 'database',
  294. onChange: (value: string) => handleInputChange('database', value),
  295. variant: 'filled',
  296. sx: {
  297. margin: (theme: Theme) => theme.spacing(0.5, 0, 0),
  298. '& .MuiFilledInput-adornedEnd': {
  299. paddingRight: 0,
  300. },
  301. },
  302. placeholder: dbTrans('database'),
  303. fullWidth: true,
  304. value: authReq.database,
  305. }}
  306. checkValid={checkIsValid}
  307. validInfo={validation}
  308. key={commonTrans('attu.database')}
  309. />
  310. {/* toggle auth */}
  311. <Box
  312. sx={{
  313. display: 'flex',
  314. width: '100%',
  315. justifyContent: 'flex-start',
  316. }}
  317. >
  318. <CustomRadio
  319. checked={withPass}
  320. label={commonTrans('attu.authentication')}
  321. handleChange={handleEnableAuth}
  322. />
  323. </Box>
  324. {/* token */}
  325. {withPass && (
  326. <>
  327. <CustomInput
  328. type="text"
  329. textConfig={{
  330. label: `${commonTrans('attu.token')} ${commonTrans('attu.optional')} `,
  331. key: 'token',
  332. onChange: (val: string) => handleInputChange('token', val),
  333. variant: 'filled',
  334. sx: {
  335. margin: (theme: Theme) => theme.spacing(0.5, 0, 0),
  336. '& .MuiFilledInput-adornedEnd': {
  337. paddingRight: 0,
  338. },
  339. },
  340. placeholder: commonTrans('attu.token'),
  341. fullWidth: true,
  342. value: authReq.token,
  343. }}
  344. checkValid={checkIsValid}
  345. validInfo={validation}
  346. key={commonTrans('attu.token')}
  347. />
  348. {/* user */}
  349. <CustomInput
  350. type="text"
  351. textConfig={{
  352. label: `${commonTrans('attu.username')} ${commonTrans('attu.optional')}`,
  353. key: 'username',
  354. onChange: (value: string) =>
  355. handleInputChange('username', value),
  356. variant: 'filled',
  357. sx: {
  358. margin: (theme: Theme) => theme.spacing(0.5, 0, 0),
  359. '& .MuiFilledInput-adornedEnd': {
  360. paddingRight: 0,
  361. },
  362. },
  363. placeholder: commonTrans('attu.username'),
  364. fullWidth: true,
  365. value: authReq.username,
  366. }}
  367. checkValid={checkIsValid}
  368. validInfo={validation}
  369. key={commonTrans('attu.username')}
  370. />
  371. {/* pass */}
  372. <CustomInput
  373. type="text"
  374. textConfig={{
  375. label: `${commonTrans('attu.password')} ${commonTrans('attu.optional')}`,
  376. key: 'password',
  377. onChange: (value: string) =>
  378. handleInputChange('password', value),
  379. variant: 'filled',
  380. sx: {
  381. margin: (theme: Theme) => theme.spacing(0.5, 0, 0),
  382. '& .MuiFilledInput-adornedEnd': {
  383. paddingRight: 0,
  384. },
  385. },
  386. placeholder: commonTrans('attu.password'),
  387. fullWidth: true,
  388. type: 'password',
  389. value: authReq.password,
  390. }}
  391. checkValid={checkIsValid}
  392. validInfo={validation}
  393. key={commonTrans('attu.password')}
  394. />
  395. </>
  396. )}
  397. {/* SSL toggle */}
  398. <Box
  399. sx={{
  400. display: 'flex',
  401. width: '100%',
  402. justifyContent: 'flex-start',
  403. }}
  404. >
  405. <FormControlLabel
  406. control={
  407. <Checkbox
  408. checked={authReq.ssl}
  409. onChange={e => handleInputChange('ssl', e.target.checked)}
  410. />
  411. }
  412. label={commonTrans('attu.ssl')}
  413. />
  414. </Box>
  415. <CustomButton type="submit" variant="contained" disabled={btnDisabled}>
  416. {btnTrans(isConnecting ? 'connecting' : 'connect')}
  417. </CustomButton>
  418. <Box
  419. sx={{
  420. display: 'flex',
  421. alignItems: 'center',
  422. marginTop: 4,
  423. '& .MuiCheckbox-root': {
  424. margin: 0,
  425. padding: '8px 4px 8px 0',
  426. },
  427. '& span': {
  428. cursor: 'pointer',
  429. fontSize: 12,
  430. fontStyle: 'italic',
  431. },
  432. }}
  433. >
  434. <label>
  435. <Checkbox
  436. size="small"
  437. checked={authReq.checkHealth}
  438. onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
  439. handleInputChange('checkHealth', e.target.checked);
  440. }}
  441. />
  442. <Typography component="span">
  443. {commonTrans('attu.checkHealth')}
  444. </Typography>
  445. </label>
  446. </Box>
  447. </Box>
  448. <Menu
  449. anchorEl={anchorEl}
  450. keepMounted
  451. sx={{
  452. // Added sx prop
  453. '& ul': {
  454. padding: 0,
  455. maxHeight: '400px',
  456. overflowY: 'auto',
  457. },
  458. }}
  459. anchorOrigin={{
  460. vertical: 'bottom',
  461. horizontal: 'right',
  462. }}
  463. transformOrigin={{
  464. vertical: 'top',
  465. horizontal: 'right',
  466. }}
  467. open={Boolean(anchorEl)}
  468. onClose={handleMenuClose}
  469. >
  470. {connections.map((connection, index) => (
  471. <Box
  472. component="li"
  473. key={index}
  474. sx={{
  475. display: 'flex',
  476. justifyContent: 'space-between',
  477. fontSize: '14px',
  478. width: 380,
  479. padding: `0 8px`,
  480. cursor: 'pointer',
  481. '&:hover': {
  482. backgroundColor: (theme: Theme) => theme.palette.action.hover,
  483. },
  484. '& .address': {
  485. display: 'grid',
  486. gridTemplateColumns: '24px 1fr',
  487. gap: 4,
  488. color: (theme: Theme) => theme.palette.text.primary,
  489. fontSize: '14px',
  490. padding: '12px 0',
  491. '& .text': {
  492. overflow: 'hidden',
  493. textOverflow: 'ellipsis',
  494. width: 200,
  495. wordWrap: 'break-word',
  496. },
  497. },
  498. '& .icon': {
  499. verticalAlign: '-3px',
  500. marginRight: 8,
  501. fontSize: '14px',
  502. },
  503. '& .time': {
  504. color: (theme: Theme) => theme.palette.text.secondary,
  505. fontSize: 11,
  506. lineHeight: 1.5,
  507. padding: '12px 0',
  508. width: 130,
  509. fontStyle: 'italic',
  510. },
  511. '& .deleteIconBtn': {
  512. padding: '8px 0',
  513. '& svg': {
  514. fontSize: '14px',
  515. },
  516. height: 16,
  517. lineHeight: '16px',
  518. margin: 0,
  519. },
  520. }}
  521. onClick={() => {
  522. handleClickOnHisotry(connection);
  523. }}
  524. >
  525. <div className="address">
  526. <Icons.link
  527. // className="icon" // Removed className
  528. sx={{
  529. // Added sx prop
  530. verticalAlign: '-5px',
  531. marginRight: (theme: Theme) => theme.spacing(1),
  532. }}
  533. ></Icons.link>
  534. <div className="text">
  535. {connection.address}/{connection.database}
  536. </div>
  537. </div>
  538. <div className="time">
  539. {connection.time !== -1
  540. ? new Date(connection.time).toLocaleString()
  541. : '--'}
  542. </div>
  543. <div>
  544. {connection.time !== -1 && (
  545. <CustomIconButton
  546. className="deleteIconBtn"
  547. onClick={e => {
  548. e.stopPropagation();
  549. handleDeleteConnection(connection);
  550. }}
  551. >
  552. <Icons.cross></Icons.cross>
  553. </CustomIconButton>
  554. )}
  555. </div>
  556. </Box>
  557. ))}
  558. </Menu>
  559. </form>
  560. );
  561. };