diff --git a/ui/src/common/SettingsDialog.tsx b/ui/src/common/SettingsDialog.tsx deleted file mode 100644 index 04125739e..000000000 --- a/ui/src/common/SettingsDialog.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, {useState} from 'react'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import TextField from '@mui/material/TextField'; -import Tooltip from '@mui/material/Tooltip'; -import {observer} from 'mobx-react-lite'; -import {useStores} from '../stores'; -import ElevationForm from './ElevationForm'; - -interface IProps { - fClose: VoidFunction; -} - -const SettingsDialog = observer(({fClose}: IProps) => { - const [pass, setPass] = useState(''); - const {currentUser, elevateStore} = useStores(); - - const handleClose = () => { - elevateStore.cleanupOidcElevate(); - fClose(); - }; - - const submitAndClose = () => { - currentUser.changePassword(pass); - fClose(); - }; - - return ( - - Change Password - - {elevateStore.elevated ? ( - setPass(e.target.value)} - fullWidth - /> - ) : ( - - )} - - - - {elevateStore.elevated && ( - -
- -
-
- )} -
-
- ); -}); - -export default SettingsDialog; diff --git a/ui/src/layout/Header.tsx b/ui/src/layout/Header.tsx index 4ffc38a02..f5429550f 100644 --- a/ui/src/layout/Header.tsx +++ b/ui/src/layout/Header.tsx @@ -9,23 +9,17 @@ import AccountCircle from '@mui/icons-material/AccountCircle'; import Chat from '@mui/icons-material/Chat'; import DevicesOther from '@mui/icons-material/DevicesOther'; import ExitToApp from '@mui/icons-material/ExitToApp'; -import Brightness4 from '@mui/icons-material/Brightness4'; -import Brightness7 from '@mui/icons-material/Brightness7'; -import BrightnessAuto from '@mui/icons-material/BrightnessAuto'; -import GitHubIcon from '@mui/icons-material/GitHub'; import MenuIcon from '@mui/icons-material/Menu'; import Apps from '@mui/icons-material/Apps'; import SupervisorAccount from '@mui/icons-material/SupervisorAccount'; +import SettingsIcon from '@mui/icons-material/Settings'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; import React, {CSSProperties} from 'react'; import {Link} from 'react-router'; import {useMediaQuery} from '@mui/material'; -import {ThemeKey} from './theme'; - -const themeIcons: Record = { - dark: , - light: , - system: , -}; const useStyles = makeStyles()((theme: Theme) => ({ appBar: { @@ -76,29 +70,13 @@ interface IProps { name: string; admin: boolean; version: string; - themeMode: ThemeKey; - toggleTheme: VoidFunction; - showSettings: VoidFunction; logout: VoidFunction; style: CSSProperties; setNavOpen: (open: boolean) => void; } -const Header = ({ - version, - name, - loggedIn, - admin, - toggleTheme, - logout, - style, - setNavOpen, - showSettings, - themeMode, -}: IProps) => { +const Header = ({version, name, loggedIn, admin, logout, style, setNavOpen}: IProps) => { const {classes} = useStyles(); - const themeLabel = `Toggle theme (current: ${themeMode})`; - const themeIcon = themeIcons[themeMode]; return ( {loggedIn && ( - + )} -
- - {themeIcon} - - - - - - - -
); }; const Buttons = ({ - showSettings, name, admin, logout, @@ -168,9 +119,10 @@ const Buttons = ({ admin: boolean; logout: VoidFunction; setNavOpen: (open: boolean) => void; - showSettings: VoidFunction; }) => { const {classes} = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + const userDropDown = Boolean(anchorEl); return (
@@ -198,17 +150,38 @@ const Buttons = ({ } label={name} - onClick={showSettings} - id="changepw" - color="inherit" - /> - } - label="Logout" - onClick={logout} - id="logout" + onClick={(e) => setAnchorEl(e.currentTarget)} + id="user-menu-button" + aria-controls={userDropDown ? 'user-menu' : undefined} + aria-haspopup="true" + aria-expanded={userDropDown ? 'true' : undefined} color="inherit" /> + setAnchorEl(null)} + anchorOrigin={{vertical: 'bottom', horizontal: 'right'}} + transformOrigin={{vertical: 'top', horizontal: 'right'}}> + setAnchorEl(null)}> + + + + Settings + + { + setAnchorEl(null); + logout(); + }}> + + + + Logout + +
); }; @@ -218,7 +191,7 @@ const ResponsiveButton: React.FC<{ sx?: ButtonProps['sx']; label: string; id?: string; - onClick?: () => void; + onClick?: (event: React.MouseEvent) => void; icon: React.ReactNode; }> = ({icon, label, ...rest}) => { const matches = useMediaQuery('(max-width:1000px)'); diff --git a/ui/src/layout/Layout.tsx b/ui/src/layout/Layout.tsx index 15f604aaa..ce12746d1 100644 --- a/ui/src/layout/Layout.tsx +++ b/ui/src/layout/Layout.tsx @@ -14,7 +14,6 @@ import {HashRouter, Navigate, Route, Routes} from 'react-router'; import Header from './Header'; import Navigation from './Navigation'; import ScrollUpButton from '../common/ScrollUpButton'; -import SettingsDialog from '../common/SettingsDialog'; import ElevationForm from '../common/ElevationForm'; import * as config from '../config'; import Applications from '../application/Applications'; @@ -22,6 +21,7 @@ import Clients from '../client/Clients'; import Plugins from '../plugin/Plugins'; import Login from '../user/Login'; import Messages from '../message/Messages'; +import Settings from '../user/Settings'; import Users from '../user/Users'; import {observer} from 'mobx-react-lite'; import {ConnectionErrorBanner} from '../common/ConnectionErrorBanner'; @@ -76,15 +76,8 @@ const Layout = observer(() => { ); const {version} = config.get('version'); const [navOpen, setNavOpen] = React.useState(false); - const [showSettings, setShowSettings] = React.useState(false); - const toggleTheme = () => { - const nextMap: Record = { - dark: 'light', - light: 'system', - system: 'dark', - }; - const next = nextMap[currentTheme]; + const setTheme = (next: ThemeKey) => { setCurrentTheme(next); localStorage.setItem(localStorageThemeKey, next); }; @@ -119,9 +112,6 @@ const Layout = observer(() => { style={{top: !connectionErrorMessage ? 0 : 64}} version={version} loggedIn={loggedIn} - themeMode={currentTheme} - toggleTheme={toggleTheme} - showSettings={() => setShowSettings(true)} logout={logout} setNavOpen={setNavOpen} /> @@ -148,6 +138,15 @@ const Layout = observer(() => { path="/users" element={authed(elevated())} /> + + )} + /> )} /> { - {showSettings && ( - setShowSettings(false)} /> - )} diff --git a/ui/src/tests/authentication.ts b/ui/src/tests/authentication.ts index 232c64718..8e8b7439a 100644 --- a/ui/src/tests/authentication.ts +++ b/ui/src/tests/authentication.ts @@ -12,10 +12,12 @@ export const login = async (page: Page, user = 'admin', pass = 'admin'): Promise await page.type($loginForm.input('.password'), pass); await page.click($loginForm.button('.login')); await waitForExists(page, selector.heading(), 'All Messages'); - await waitForExists(page, 'button', 'logout'); }; export const logout = async (page: Page): Promise => { + await page.waitForSelector('#user-menu-button'); + await page.click('#user-menu-button'); + await page.waitForSelector('#logout'); await page.click('#logout'); await waitForExists(page, selector.heading(), 'Login'); expect(page.url()).toContain('/login'); diff --git a/ui/src/tests/oidc.test.ts b/ui/src/tests/oidc.test.ts index 527b25c45..df56364e4 100644 --- a/ui/src/tests/oidc.test.ts +++ b/ui/src/tests/oidc.test.ts @@ -31,7 +31,6 @@ const loginWithOIDC = async (page: Page, user: DexUser): Promise => { const expectLoggedIn = async (page: Page): Promise => { await waitForExists(page, selector.heading(), 'All Messages'); - await page.waitForSelector('#logout'); }; const oidcError = async (page: Page): Promise => { diff --git a/ui/src/tests/user.test.ts b/ui/src/tests/user.test.ts index 4d9cacebb..792bd74fd 100644 --- a/ui/src/tests/user.test.ts +++ b/ui/src/tests/user.test.ts @@ -1,6 +1,6 @@ import {Page} from 'puppeteer'; import {newTest, GotifyTest} from './setup'; -import {clearField, count, innerText, waitForExists, waitToDisappear} from './utils'; +import {clearField, clickByText, count, innerText, waitForExists, waitToDisappear} from './utils'; import {afterAll, beforeAll, describe, expect, it} from 'vitest'; import * as auth from './authentication'; import * as selector from './selector'; @@ -120,8 +120,12 @@ describe('User', () => { expect(await count(page, $table.rows())).toBe(3); }); it('changes password of current user', async () => { - const $changepw = selector.form('#changepw-dialog'); - await page.click('#changepw'); + const $changepw = selector.form('#changepw-form'); + await page.waitForSelector('#user-menu-button'); + await page.click('#user-menu-button'); + await clickByText(page, 'a', 'Settings'); + await waitToDisappear(page, '.MuiBackdrop-root'); + await waitForExists(page, selector.heading(), 'Settings'); await page.waitForSelector($changepw.selector()); await page.type($changepw.input('.newpass'), 'changed'); await page.click($changepw.button('.change')); diff --git a/ui/src/user/Settings.tsx b/ui/src/user/Settings.tsx new file mode 100644 index 000000000..ad3b0aeb8 --- /dev/null +++ b/ui/src/user/Settings.tsx @@ -0,0 +1,111 @@ +import React, {useState} from 'react'; +import Button from '@mui/material/Button'; +import FormControl from '@mui/material/FormControl'; +import Grid from '@mui/material/Grid'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Paper from '@mui/material/Paper'; +import Select from '@mui/material/Select'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import {observer} from 'mobx-react-lite'; +import DefaultPage from '../common/DefaultPage'; +import ElevationForm from '../common/ElevationForm'; +import {ThemeKey} from '../layout/theme'; +import {useStores} from '../stores'; +import * as config from '../config'; + +interface IProps { + themeMode: ThemeKey; + setTheme: (theme: ThemeKey) => void; +} + +const Settings = observer(({themeMode, setTheme}: IProps) => { + return ( + + + + + Appearance + + + Theme + + + + + + + + Change Password + + + + + + ); +}); + +const ChangePasswordForm = () => { + const [pass, setPass] = useState(''); + const {currentUser, elevateStore} = useStores(); + const localAuthEnabled = config.get('localAuth'); + + const submit = () => { + currentUser.changePassword(pass); + setPass(''); + }; + + if (!localAuthEnabled) { + return Password login is disabled on this server.; + } + + if (!elevateStore.elevated) { + return ; + } + + return ( +
{ + e.preventDefault(); + submit(); + }}> + setPass(e.target.value)} + fullWidth + /> + +
+ +
+
+ + ); +}; + +export default Settings;