AuthForm.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. import React, { useContext, useEffect, useMemo, useState } from 'react';
  2. import Typography from '@mui/material/Typography';
  3. import Checkbox from '@mui/material/Checkbox';
  4. import { useTranslation } from 'react-i18next';
  5. import CustomButton from '@/components/customButton/CustomButton';
  6. import CustomInput from '@/components/customInput/CustomInput';
  7. import { useFormValidation } from '@/hooks';
  8. import { formatForm } from '@/utils';
  9. import { useNavigate } from 'react-router-dom';
  10. import { rootContext, authContext, dataContext } from '@/context';
  11. import {
  12. ATTU_AUTH_HISTORY,
  13. MILVUS_DATABASE,
  14. MILVUS_SERVERS,
  15. MILVUS_URL,
  16. } from '@/consts';
  17. import { CustomRadio } from '@/components/customRadio/CustomRadio';
  18. import Icons from '@/components/icons/Icons';
  19. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  20. import CustomIconButton from '@/components/customButton/CustomIconButton';
  21. import type { AuthReq } from '@server/types';
  22. import FormControlLabel from '@mui/material/FormControlLabel';
  23. import Box from '@mui/material/Box';
  24. import type { Theme } from '@mui/material';
  25. import Autocomplete from '@mui/material/Autocomplete';
  26. import TextField from '@mui/material/TextField';
  27. // Add Connection type definition back
  28. type Connection = AuthReq & {
  29. time: number;
  30. };
  31. // Parse server list from environment variables
  32. const parseFixedConnections = (): Connection[] => {
  33. const serverList = MILVUS_SERVERS || '';
  34. const defaultUrl = MILVUS_URL || '127.0.0.1:19530';
  35. const defaultDatabase = MILVUS_DATABASE;
  36. // If MILVUS_SERVERS exists and is not empty, parse it
  37. if (serverList && serverList.trim() !== '') {
  38. return serverList.split(',').map((server: string) => {
  39. const [address, database] = server.trim().split('/');
  40. return {
  41. address: address.trim(),
  42. database: database?.trim() || defaultDatabase,
  43. token: '',
  44. username: '',
  45. password: '',
  46. ssl: false,
  47. checkHealth: true,
  48. time: -1,
  49. clientId: '',
  50. };
  51. });
  52. }
  53. // If MILVUS_SERVERS is empty or doesn't exist, use MILVUS_URL
  54. return [
  55. {
  56. address: defaultUrl,
  57. database: defaultDatabase,
  58. token: '',
  59. username: '',
  60. password: '',
  61. ssl: false,
  62. checkHealth: true,
  63. time: -1,
  64. clientId: '',
  65. },
  66. ];
  67. };
  68. // Get fixed connections from environment variables
  69. const FIXED_CONNECTIONS: Connection[] = parseFixedConnections();
  70. export const AuthForm = () => {
  71. // context
  72. const { openSnackBar } = useContext(rootContext);
  73. const { authReq, setAuthReq, login } = useContext(authContext);
  74. const { setDatabase } = useContext(dataContext);
  75. // i18n
  76. const { t: commonTrans } = useTranslation();
  77. const { t: btnTrans } = useTranslation('btn');
  78. const { t: successTrans } = useTranslation('success');
  79. const { t: dbTrans } = useTranslation('database');
  80. // hooks
  81. const navigate = useNavigate();
  82. // UI states
  83. const [withPass, setWithPass] = useState(false);
  84. const [connections, setConnections] = useState<Connection[]>([]);
  85. const [isConnecting, setIsConnecting] = useState(false);
  86. // form validation
  87. const checkedForm = useMemo(() => {
  88. return formatForm(authReq);
  89. }, [authReq]);
  90. const { validation, checkIsValid, resetValidation } =
  91. useFormValidation(checkedForm);
  92. // UI handlers
  93. // handle input change
  94. const handleInputChange = (
  95. key:
  96. | 'address'
  97. | 'username'
  98. | 'password'
  99. | 'ssl'
  100. | 'database'
  101. | 'token'
  102. | 'checkHealth',
  103. value: string | boolean
  104. ) => {
  105. if (key === 'address' && typeof value === 'string') {
  106. // Check if address contains database name (format: address/database)
  107. const parts = value.split('/');
  108. if (parts.length === 2) {
  109. setAuthReq(v => ({
  110. ...v,
  111. address: parts[0],
  112. database: parts[1],
  113. }));
  114. return;
  115. }
  116. }
  117. setAuthReq(v => ({ ...v, [key]: value }));
  118. };
  119. // handle auth toggle
  120. const handleEnableAuth = (val: boolean) => {
  121. setWithPass(val);
  122. };
  123. // handle connect
  124. const handleConnect = async (event: React.FormEvent) => {
  125. event.preventDefault();
  126. // set connecting
  127. setIsConnecting(true);
  128. try {
  129. // login
  130. const loginParams = { ...authReq };
  131. if (!withPass) {
  132. loginParams.username = '';
  133. loginParams.password = '';
  134. loginParams.token = '';
  135. }
  136. await login(loginParams);
  137. // set database
  138. setDatabase(authReq.database);
  139. // success message
  140. openSnackBar(successTrans('connect'));
  141. // get connection history
  142. const history = JSON.parse(
  143. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  144. );
  145. // add new connection to history, filter out the same connection
  146. const newHistory = [
  147. ...history.filter(
  148. (item: any) =>
  149. item.address !== authReq.address ||
  150. item.database !== authReq.database
  151. ),
  152. {
  153. address: authReq.address,
  154. database: authReq.database,
  155. username: authReq.username,
  156. password: authReq.password,
  157. token: authReq.token,
  158. time: Date.now(),
  159. checkHealth: authReq.checkHealth,
  160. },
  161. ];
  162. // if the count of history connections are more than 16, remove the first one, but it should keep the default one
  163. if (newHistory.length > 16) {
  164. newHistory.shift();
  165. }
  166. // save to local storage
  167. window.localStorage.setItem(
  168. ATTU_AUTH_HISTORY,
  169. JSON.stringify(newHistory)
  170. );
  171. // set title
  172. document.title = authReq.address ? `${authReq.address} - Attu` : 'Attu';
  173. // redirect to homepage
  174. navigate('/');
  175. } catch (error: any) {
  176. // if not authorized, show auth inputs
  177. if (error.response.data.message.includes('UNAUTHENTICATED')) {
  178. handleEnableAuth(true);
  179. }
  180. } finally {
  181. setIsConnecting(false);
  182. }
  183. };
  184. // connect history clicked
  185. const handleClickOnHisotry = (connection: Connection) => {
  186. // set auth request
  187. setAuthReq(connection);
  188. };
  189. const handleDeleteConnection = (connection: Connection) => {
  190. // Don't allow deletion of fixed connections
  191. if (
  192. FIXED_CONNECTIONS.some(
  193. fixed =>
  194. fixed.address === connection.address &&
  195. fixed.database === connection.database
  196. )
  197. ) {
  198. return;
  199. }
  200. const history = JSON.parse(
  201. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  202. ) as Connection[];
  203. const newHistory = history.filter(
  204. item =>
  205. item.address !== connection.address ||
  206. item.database !== connection.database
  207. );
  208. if (newHistory.length === 0) {
  209. newHistory.push(FIXED_CONNECTIONS[0]);
  210. }
  211. // save to local storage
  212. window.localStorage.setItem(ATTU_AUTH_HISTORY, JSON.stringify(newHistory));
  213. // sort by time
  214. newHistory.sort((a, b) => {
  215. return new Date(b.time).getTime() - new Date(a.time).getTime();
  216. });
  217. // Combine fixed and history connections
  218. setConnections([...FIXED_CONNECTIONS, ...newHistory]);
  219. };
  220. // Add clear all history handler
  221. const handleClearAllHistory = () => {
  222. // Save only the default connection
  223. const newHistory = [FIXED_CONNECTIONS[0]];
  224. window.localStorage.setItem(ATTU_AUTH_HISTORY, JSON.stringify(newHistory));
  225. // Combine fixed and history connections
  226. setConnections([...FIXED_CONNECTIONS, ...newHistory]);
  227. // Reset the form to default values
  228. setAuthReq(FIXED_CONNECTIONS[0]);
  229. };
  230. // is button should be disabled
  231. const btnDisabled = authReq.address.trim().length === 0 || isConnecting;
  232. // load connection from local storage
  233. useEffect(() => {
  234. const historyConnections: Connection[] = JSON.parse(
  235. window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
  236. );
  237. // Start with fixed connections
  238. const allConnections = [...FIXED_CONNECTIONS];
  239. // Add history connections, filtering out any that match fixed connections
  240. const uniqueHistoryConnections = historyConnections.filter(
  241. historyConn =>
  242. !FIXED_CONNECTIONS.some(
  243. fixedConn =>
  244. fixedConn.address === historyConn.address &&
  245. fixedConn.database === historyConn.database
  246. )
  247. );
  248. allConnections.push(...uniqueHistoryConnections);
  249. // Sort by time
  250. allConnections.sort((a, b) => {
  251. return new Date(b.time).getTime() - new Date(a.time).getTime();
  252. });
  253. setConnections(allConnections);
  254. }, []);
  255. // UI effect
  256. useEffect(() => {
  257. // if address contains zilliz, or username or password is not empty
  258. // set withpass to true
  259. const withPass =
  260. (authReq.address.length > 0 && authReq.address.includes('zilliz')) ||
  261. authReq.username.length > 0 ||
  262. authReq.password.length > 0;
  263. // set with pass
  264. setWithPass(withPass);
  265. // reset form
  266. resetValidation(formatForm(authReq));
  267. }, [authReq.address, authReq.username, authReq.password]);
  268. return (
  269. <form onSubmit={handleConnect}>
  270. <Box
  271. sx={{
  272. display: 'flex',
  273. flexDirection: 'column',
  274. padding: (theme: Theme) => theme.spacing(0, 3),
  275. position: 'relative',
  276. height: '100%',
  277. }}
  278. >
  279. <Box
  280. sx={{
  281. textAlign: 'left',
  282. alignSelf: 'flex-start',
  283. padding: (theme: Theme) => theme.spacing(2, 0, 1.5),
  284. '& svg': {
  285. fontSize: 16,
  286. marginLeft: (theme: Theme) => theme.spacing(0.5),
  287. color: 'primary.main',
  288. },
  289. }}
  290. >
  291. <Typography
  292. variant="h4"
  293. component="h4"
  294. sx={{
  295. fontSize: 20,
  296. fontWeight: 600,
  297. color: 'text.primary',
  298. }}
  299. >
  300. {commonTrans('attu.connectTitle')}
  301. <CustomToolTip title={commonTrans('attu.connectionTip')}>
  302. <Icons.info />
  303. </CustomToolTip>
  304. </Typography>
  305. </Box>
  306. {/* Replace address input with Autocomplete */}
  307. <Autocomplete
  308. freeSolo={true}
  309. options={connections}
  310. getOptionLabel={option =>
  311. typeof option === 'string'
  312. ? option
  313. : `${option.address}/${option.database}`
  314. }
  315. value={connections.find(c => c.address === authReq.address) || null}
  316. onChange={(event, newValue) => {
  317. if (newValue) {
  318. if (typeof newValue === 'string') {
  319. // Handle free text input
  320. const [address, database] = newValue.split('/');
  321. setAuthReq(v => ({
  322. ...v,
  323. address: address.trim(),
  324. database: database?.trim() || MILVUS_DATABASE,
  325. }));
  326. } else {
  327. handleClickOnHisotry(newValue);
  328. }
  329. }
  330. }}
  331. onInputChange={(event, newInputValue) => {
  332. // Only update if the input matches a valid connection
  333. const matchingConnection = connections.find(
  334. c =>
  335. `${c.address}/${c.database}`.toLowerCase() ===
  336. newInputValue.toLowerCase()
  337. );
  338. if (matchingConnection) {
  339. handleClickOnHisotry(matchingConnection);
  340. } else if (newInputValue) {
  341. // Handle free text input
  342. const [address, database] = newInputValue.split('/');
  343. setAuthReq(v => ({
  344. ...v,
  345. address: address.trim(),
  346. database: database?.trim() || MILVUS_DATABASE,
  347. }));
  348. }
  349. }}
  350. filterOptions={(options, state) => {
  351. // Only filter when there's input text
  352. if (!state.inputValue) {
  353. return options;
  354. }
  355. return options.filter(option =>
  356. `${option.address}/${option.database}`
  357. .toLowerCase()
  358. .includes(state.inputValue.toLowerCase())
  359. );
  360. }}
  361. renderInput={params => (
  362. <TextField
  363. {...params}
  364. label={commonTrans('attu.address')}
  365. variant="filled"
  366. required
  367. error={validation.address?.result}
  368. helperText={validation.address?.errText}
  369. sx={{
  370. margin: (theme: Theme) => theme.spacing(0.5, 0),
  371. '& .MuiFilledInput-root': {
  372. backgroundColor: 'background.default',
  373. '&:hover': {
  374. backgroundColor: 'action.hover',
  375. },
  376. '&.Mui-focused': {
  377. backgroundColor: 'background.default',
  378. },
  379. },
  380. }}
  381. />
  382. )}
  383. ListboxProps={{
  384. sx: {
  385. '& .MuiAutocomplete-listbox': {
  386. padding: 0,
  387. '& li': {
  388. padding: (theme: Theme) => theme.spacing(1.5, 2),
  389. '&:not(:last-child)': {
  390. borderBottom: (theme: Theme) =>
  391. `1px solid ${theme.palette.divider}`,
  392. },
  393. },
  394. },
  395. },
  396. }}
  397. renderOption={(props, option) => {
  398. // Extract key from props
  399. const { key, ...otherProps } = props;
  400. // If it's the last option and there are multiple connections, add clear history option
  401. if (
  402. option === connections[connections.length - 1] &&
  403. connections.length > 1
  404. ) {
  405. return (
  406. <React.Fragment key={`${option.address}-${option.database}`}>
  407. <Box
  408. component="li"
  409. {...otherProps}
  410. sx={{
  411. display: 'flex',
  412. justifyContent: 'space-between',
  413. fontSize: '14px',
  414. '&:hover': {
  415. backgroundColor: (theme: Theme) =>
  416. theme.palette.action.hover,
  417. },
  418. }}
  419. >
  420. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  421. <Icons.link sx={{ fontSize: 16 }} />
  422. <Typography>
  423. {option.address}/{option.database}
  424. </Typography>
  425. {(option.username || option.password || option.token) && (
  426. <Icons.key
  427. sx={{
  428. fontSize: 14,
  429. color: 'text.secondary',
  430. ml: 0.5,
  431. }}
  432. />
  433. )}
  434. </Box>
  435. {option.time !== -1 &&
  436. !FIXED_CONNECTIONS.some(
  437. fixed =>
  438. fixed.address === option.address &&
  439. fixed.database === option.database
  440. ) && (
  441. <Box
  442. sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
  443. >
  444. <Typography
  445. sx={{
  446. fontSize: 11,
  447. color: 'text.secondary',
  448. fontStyle: 'italic',
  449. }}
  450. >
  451. {new Date(option.time).toLocaleString()}
  452. </Typography>
  453. <CustomIconButton
  454. onClick={e => {
  455. e.stopPropagation();
  456. handleDeleteConnection(option);
  457. }}
  458. sx={{ padding: '4px' }}
  459. >
  460. <Icons.cross sx={{ fontSize: 14 }} />
  461. </CustomIconButton>
  462. </Box>
  463. )}
  464. </Box>
  465. <Box
  466. component="li"
  467. onClick={handleClearAllHistory}
  468. sx={{
  469. display: 'flex',
  470. alignItems: 'center',
  471. gap: 1,
  472. fontSize: '12px',
  473. borderTop: (theme: Theme) =>
  474. `1px solid ${theme.palette.divider}`,
  475. color: 'error.main',
  476. cursor: 'pointer',
  477. padding: (theme: Theme) => theme.spacing(1),
  478. marginTop: (theme: Theme) => theme.spacing(1),
  479. backgroundColor: (theme: Theme) =>
  480. theme.palette.background.default,
  481. '&:hover': {
  482. backgroundColor: (theme: Theme) =>
  483. theme.palette.action.hover,
  484. },
  485. }}
  486. >
  487. <Icons.delete sx={{ fontSize: 14 }} />
  488. <Typography sx={{ fontWeight: 500 }}>
  489. {commonTrans('attu.clearHistory')}
  490. </Typography>
  491. </Box>
  492. </React.Fragment>
  493. );
  494. }
  495. // Regular connection option
  496. return (
  497. <Box
  498. component="li"
  499. key={`${option.address}-${option.database}`}
  500. {...otherProps}
  501. sx={{
  502. display: 'flex',
  503. justifyContent: 'space-between',
  504. fontSize: '14px',
  505. padding: (theme: Theme) => theme.spacing(1.5, 2),
  506. '&:hover': {
  507. backgroundColor: (theme: Theme) =>
  508. theme.palette.action.hover,
  509. },
  510. }}
  511. >
  512. <Box
  513. sx={{
  514. display: 'flex',
  515. alignItems: 'center',
  516. gap: 1,
  517. minWidth: 0,
  518. flex: 1,
  519. }}
  520. >
  521. <Icons.link sx={{ fontSize: 16, flexShrink: 0, mt: 0.5 }} />
  522. <Typography
  523. sx={{
  524. wordBreak: 'break-all',
  525. lineHeight: 1.5,
  526. display: 'flex',
  527. alignItems: 'center',
  528. gap: 0.5,
  529. }}
  530. >
  531. {option.address}/{option.database}
  532. {(option.username || option.password || option.token) && (
  533. <Icons.key
  534. sx={{
  535. fontSize: 14,
  536. color: 'text.secondary',
  537. flexShrink: 0,
  538. }}
  539. />
  540. )}
  541. </Typography>
  542. </Box>
  543. {option.time !== -1 && (
  544. <Box
  545. sx={{
  546. display: 'flex',
  547. alignItems: 'center',
  548. gap: 2,
  549. minWidth: 200,
  550. justifyContent: 'flex-end',
  551. }}
  552. >
  553. <Typography
  554. sx={{
  555. fontSize: 11,
  556. color: 'text.secondary',
  557. fontStyle: 'italic',
  558. whiteSpace: 'nowrap',
  559. }}
  560. >
  561. {new Date(option.time).toLocaleString()}
  562. </Typography>
  563. <CustomIconButton
  564. onClick={e => {
  565. e.stopPropagation();
  566. handleDeleteConnection(option);
  567. }}
  568. sx={{
  569. padding: '4px',
  570. marginLeft: 1,
  571. }}
  572. >
  573. <Icons.cross sx={{ fontSize: 14 }} />
  574. </CustomIconButton>
  575. </Box>
  576. )}
  577. </Box>
  578. );
  579. }}
  580. />
  581. {/* db */}
  582. <CustomInput
  583. type="text"
  584. textConfig={{
  585. label: `Milvus ${dbTrans('database')} ${commonTrans('attu.optional')}`,
  586. key: 'database',
  587. onChange: (value: string) => handleInputChange('database', value),
  588. variant: 'filled',
  589. sx: {
  590. margin: (theme: Theme) => theme.spacing(0.5, 0),
  591. '& .MuiFilledInput-root': {
  592. backgroundColor: 'background.default',
  593. '&:hover': {
  594. backgroundColor: 'action.hover',
  595. },
  596. '&.Mui-focused': {
  597. backgroundColor: 'background.default',
  598. },
  599. },
  600. },
  601. placeholder: dbTrans('database'),
  602. fullWidth: true,
  603. value: authReq.database,
  604. }}
  605. checkValid={checkIsValid}
  606. validInfo={validation}
  607. key={commonTrans('attu.database')}
  608. />
  609. {/* toggle auth */}
  610. <Box
  611. sx={{
  612. display: 'flex',
  613. width: '100%',
  614. justifyContent: 'flex-start',
  615. marginTop: (theme: Theme) => theme.spacing(1),
  616. }}
  617. >
  618. <CustomRadio
  619. checked={withPass}
  620. label={commonTrans('attu.authentication')}
  621. handleChange={handleEnableAuth}
  622. />
  623. </Box>
  624. {/* token */}
  625. {withPass && (
  626. <>
  627. <CustomInput
  628. type="text"
  629. textConfig={{
  630. label: `${commonTrans('attu.token')} ${commonTrans('attu.optional')} `,
  631. key: 'token',
  632. onChange: (val: string) => handleInputChange('token', val),
  633. variant: 'filled',
  634. sx: {
  635. margin: (theme: Theme) => theme.spacing(0.5, 0),
  636. '& .MuiFilledInput-root': {
  637. backgroundColor: 'background.default',
  638. '&:hover': {
  639. backgroundColor: 'action.hover',
  640. },
  641. '&.Mui-focused': {
  642. backgroundColor: 'background.default',
  643. },
  644. },
  645. },
  646. placeholder: commonTrans('attu.token'),
  647. fullWidth: true,
  648. value: authReq.token,
  649. }}
  650. checkValid={checkIsValid}
  651. validInfo={validation}
  652. key={commonTrans('attu.token')}
  653. />
  654. {/* user */}
  655. <CustomInput
  656. type="text"
  657. textConfig={{
  658. label: `${commonTrans('attu.username')} ${commonTrans('attu.optional')}`,
  659. key: 'username',
  660. onChange: (value: string) =>
  661. handleInputChange('username', value),
  662. variant: 'filled',
  663. sx: {
  664. margin: (theme: Theme) => theme.spacing(0.5, 0),
  665. '& .MuiFilledInput-root': {
  666. backgroundColor: 'background.default',
  667. '&:hover': {
  668. backgroundColor: 'action.hover',
  669. },
  670. '&.Mui-focused': {
  671. backgroundColor: 'background.default',
  672. },
  673. },
  674. },
  675. placeholder: commonTrans('attu.username'),
  676. fullWidth: true,
  677. value: authReq.username,
  678. }}
  679. checkValid={checkIsValid}
  680. validInfo={validation}
  681. key={commonTrans('attu.username')}
  682. />
  683. {/* pass */}
  684. <CustomInput
  685. type="text"
  686. textConfig={{
  687. label: `${commonTrans('attu.password')} ${commonTrans('attu.optional')}`,
  688. key: 'password',
  689. onChange: (value: string) =>
  690. handleInputChange('password', value),
  691. variant: 'filled',
  692. sx: {
  693. margin: (theme: Theme) => theme.spacing(0.5, 0),
  694. '& .MuiFilledInput-root': {
  695. backgroundColor: 'background.default',
  696. '&:hover': {
  697. backgroundColor: 'action.hover',
  698. },
  699. '&.Mui-focused': {
  700. backgroundColor: 'background.default',
  701. },
  702. },
  703. },
  704. placeholder: commonTrans('attu.password'),
  705. fullWidth: true,
  706. type: 'password',
  707. value: authReq.password,
  708. }}
  709. checkValid={checkIsValid}
  710. validInfo={validation}
  711. key={commonTrans('attu.password')}
  712. />
  713. </>
  714. )}
  715. <Box
  716. sx={{
  717. marginTop: 'auto',
  718. padding: (theme: Theme) => theme.spacing(2, 0),
  719. }}
  720. >
  721. <Box
  722. sx={{
  723. display: 'flex',
  724. alignItems: 'center',
  725. gap: (theme: Theme) => theme.spacing(2),
  726. }}
  727. >
  728. <CustomButton
  729. type="submit"
  730. variant="contained"
  731. disabled={btnDisabled}
  732. sx={{
  733. height: 36,
  734. fontSize: 14,
  735. fontWeight: 500,
  736. flex: 1,
  737. }}
  738. >
  739. {btnTrans(isConnecting ? 'connecting' : 'connect')}
  740. </CustomButton>
  741. <Box
  742. sx={{
  743. display: 'flex',
  744. alignItems: 'center',
  745. gap: (theme: Theme) => theme.spacing(1),
  746. borderLeft: (theme: Theme) =>
  747. `1px solid ${theme.palette.divider}`,
  748. paddingLeft: (theme: Theme) => theme.spacing(2),
  749. }}
  750. >
  751. <FormControlLabel
  752. control={
  753. <Checkbox
  754. checked={authReq?.ssl ?? false}
  755. onChange={e => handleInputChange('ssl', e.target.checked)}
  756. sx={{
  757. padding: '4px',
  758. '&.Mui-checked': {
  759. color: 'primary.main',
  760. },
  761. }}
  762. />
  763. }
  764. label={
  765. <Typography
  766. sx={{
  767. fontSize: 13,
  768. color: 'text.secondary',
  769. }}
  770. >
  771. {commonTrans('attu.ssl')}
  772. </Typography>
  773. }
  774. />
  775. <FormControlLabel
  776. control={
  777. <Checkbox
  778. size="small"
  779. checked={authReq?.checkHealth ?? true}
  780. onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
  781. handleInputChange('checkHealth', e.target.checked);
  782. }}
  783. sx={{
  784. padding: '4px',
  785. '&.Mui-checked': {
  786. color: 'primary.main',
  787. },
  788. }}
  789. />
  790. }
  791. label={
  792. <Typography
  793. sx={{
  794. fontSize: 13,
  795. color: 'text.secondary',
  796. }}
  797. >
  798. {commonTrans('attu.checkHealth')}
  799. </Typography>
  800. }
  801. />
  802. </Box>
  803. </Box>
  804. </Box>
  805. </Box>
  806. </form>
  807. );
  808. };