(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"
/>
+
);
};
@@ -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 (
+
+ );
+};
+
+export default Settings;