Header.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { FC, useContext, useState, MouseEvent } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import Typography from '@mui/material/Typography';
  4. import Tooltip from '@mui/material/Tooltip';
  5. import Menu from '@mui/material/Menu';
  6. import MenuItem from '@mui/material/MenuItem';
  7. import { useNavigate } from 'react-router-dom';
  8. import { navContext, dataContext, authContext, rootContext } from '@/context';
  9. import { MilvusService } from '@/http';
  10. import CustomSelector from '@/components/customSelector/CustomSelector';
  11. import StatusIcon from '@/components/status/StatusIcon';
  12. import UpdateUser from '@/pages/user/dialogs/UpdateUserPassDialog';
  13. import icons from '../icons/Icons';
  14. import { styled } from '@mui/material/styles';
  15. import IconButton from '@mui/material/IconButton';
  16. import { ColorModeContext } from '@/context';
  17. import { LoadingType } from '@/components/status/StatusIcon';
  18. const HeaderWrapper = styled('header')(({ theme }) => ({
  19. display: 'flex',
  20. alignItems: 'center',
  21. color: theme.palette.text.primary,
  22. backgroundColor: theme.palette.background.paper,
  23. paddingRight: theme.spacing(1),
  24. borderBottom: `1px solid ${theme.palette.divider}`,
  25. height: 48,
  26. }));
  27. const ContentWrapper = styled('div')({
  28. display: 'flex',
  29. justifyContent: 'space-between',
  30. alignItems: 'center',
  31. flex: 1,
  32. height: 48,
  33. });
  34. const Navigation = styled('div')({
  35. display: 'flex',
  36. alignItems: 'center',
  37. });
  38. const StyledIcon = styled('div')(({ theme }) => ({
  39. color: theme.palette.primary.main,
  40. cursor: 'pointer',
  41. marginRight: theme.spacing(1),
  42. }));
  43. const AddressWrapper = styled('div')(({ theme }) => ({
  44. display: 'flex',
  45. alignItems: 'center',
  46. '& .text': {
  47. marginRight: theme.spacing(2),
  48. '& .address': {
  49. fontSize: '12px',
  50. lineHeight: 1.3,
  51. },
  52. '& .status': {
  53. fontSize: '12px',
  54. lineHeight: 1.3,
  55. color: '#1ba954',
  56. },
  57. },
  58. }));
  59. const Title = styled(Typography)(({ theme }) => ({
  60. paddingLeft: theme.spacing(2),
  61. }));
  62. const DatabaseSelector = styled(CustomSelector)(({ theme }) => ({
  63. transform: 'translateY(-4px)',
  64. width: 'auto',
  65. minWidth: 120,
  66. '& .MuiInputLabel-root': {
  67. top: '4px',
  68. },
  69. }));
  70. const ModeButton = styled(IconButton)(({ theme }) => ({
  71. marginRight: theme.spacing(1),
  72. '& svg': {
  73. fontSize: 18,
  74. color: theme.palette.text.primary,
  75. },
  76. }));
  77. const Extra = styled('span')(({ theme }) => ({
  78. marginLeft: theme.spacing(0.5),
  79. display: 'flex',
  80. '& svg': {
  81. fontSize: 15,
  82. color: theme.palette.primary.main,
  83. },
  84. }));
  85. const Header: FC = () => {
  86. // use context
  87. const { navInfo } = useContext(navContext);
  88. const { mode, toggleColorMode } = useContext(ColorModeContext);
  89. const { database, databases, setDatabase, loading } = useContext(dataContext);
  90. const { authReq, logout } = useContext(authContext);
  91. const { setDialog, handleCloseDialog, openSnackBar } =
  92. useContext(rootContext);
  93. const { address, username } = authReq;
  94. const navigate = useNavigate();
  95. // UI states
  96. const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
  97. // i8n
  98. const { t: commonTrans } = useTranslation();
  99. const statusTrans = commonTrans('status');
  100. const { t: dbTrans } = useTranslation('database');
  101. const { t: successTrans } = useTranslation('success');
  102. const { t: userTrans } = useTranslation('user');
  103. // icons
  104. const BackIcon = icons.back;
  105. const LogoutIcon = icons.logout;
  106. const Avatar = icons.avatar;
  107. // UI handlers
  108. const handleBack = (path: string) => {
  109. navigate(path);
  110. };
  111. const handleLogout = async () => {
  112. logout(false);
  113. };
  114. const useDatabase = async (database: string) => {
  115. await MilvusService.useDatabase({ database });
  116. };
  117. const handleUserMenuClick = (event: MouseEvent<HTMLDivElement>) => {
  118. setAnchorEl(event.currentTarget);
  119. };
  120. const handleUserMenuClose = () => {
  121. setAnchorEl(null);
  122. };
  123. const handleChangePassword = () => {
  124. setAnchorEl(null);
  125. setDialog({
  126. open: true,
  127. type: 'custom',
  128. params: {
  129. component: (
  130. <UpdateUser
  131. username={username}
  132. onUpdate={res => {
  133. if (res.error_code === 'Success') {
  134. openSnackBar(successTrans('passwordChanged'));
  135. handleCloseDialog();
  136. setAnchorEl(null);
  137. logout();
  138. } else {
  139. openSnackBar(res.detail, 'error');
  140. }
  141. }}
  142. handleClose={handleCloseDialog}
  143. />
  144. ),
  145. },
  146. });
  147. };
  148. // local computes
  149. const dbOptions = databases.map(d => ({ value: d.name, label: d.name }));
  150. const isLoadingDb = dbOptions.length === 0;
  151. return (
  152. <HeaderWrapper>
  153. <ContentWrapper>
  154. <Navigation>
  155. {navInfo.backPath !== '' && (
  156. <StyledIcon onClick={() => handleBack(navInfo.backPath)}>
  157. <BackIcon />
  158. </StyledIcon>
  159. )}
  160. {navInfo.showDatabaseSelector &&
  161. (!isLoadingDb ? (
  162. <DatabaseSelector
  163. label={dbTrans('database')}
  164. value={database}
  165. onChange={async (e: { target: { value: unknown } }) => {
  166. const database = e.target.value as string;
  167. await useDatabase(database);
  168. setDatabase(database);
  169. // if url contains databases, go to the database page
  170. if (window.location.hash.includes('databases')) {
  171. navigate(`/databases/${database}/collections`);
  172. }
  173. }}
  174. options={dbOptions}
  175. variant="filled"
  176. disabled={loading}
  177. />
  178. ) : (
  179. <StatusIcon type={LoadingType.CREATING} />
  180. ))}
  181. <Title variant="h5" color="textPrimary">
  182. {navInfo.navTitle}
  183. </Title>
  184. <Extra>{navInfo.extra}</Extra>
  185. </Navigation>
  186. <AddressWrapper>
  187. <ModeButton onClick={toggleColorMode} color="inherit">
  188. {mode === 'dark' ? <icons.night /> : <icons.day />}
  189. </ModeButton>
  190. <div className="text">
  191. <Typography className="address">{address}</Typography>
  192. <Typography className="status">{statusTrans.running}</Typography>
  193. </div>
  194. {username && (
  195. <>
  196. <Tooltip title={username}>
  197. <StyledIcon
  198. onClick={handleUserMenuClick}
  199. style={{ cursor: 'pointer' }}
  200. >
  201. <Avatar />
  202. </StyledIcon>
  203. </Tooltip>
  204. <Menu
  205. anchorEl={anchorEl}
  206. open={Boolean(anchorEl)}
  207. onClose={handleUserMenuClose}
  208. anchorOrigin={{
  209. vertical: 'bottom',
  210. horizontal: 'right',
  211. }}
  212. transformOrigin={{
  213. vertical: 'top',
  214. horizontal: 'right',
  215. }}
  216. >
  217. <MenuItem onClick={handleChangePassword}>
  218. {userTrans('changePassword')}
  219. </MenuItem>
  220. </Menu>
  221. </>
  222. )}
  223. <Tooltip title={'disconnect'}>
  224. <StyledIcon>
  225. <LogoutIcon onClick={handleLogout} />
  226. </StyledIcon>
  227. </Tooltip>
  228. </AddressWrapper>
  229. </ContentWrapper>
  230. </HeaderWrapper>
  231. );
  232. };
  233. export default Header;