From dbace59979273534af2957df564df6649feac5dc Mon Sep 17 00:00:00 2001 From: David Cho-Lerat Date: Wed, 24 Jan 2024 18:27:33 +0100 Subject: [PATCH] SONAR-21482 User security page adopts the new UI --- .../src/main/js/apps/account/Account.tsx | 33 +++- .../js/apps/account/__tests__/Account-it.tsx | 27 ++- .../js/apps/account/security/Security.tsx | 20 ++- .../main/js/apps/account/security/Tokens.tsx | 15 +- .../js/apps/users/components/TokensForm.tsx | 25 +-- .../components/common/ResetPasswordForm.tsx | 166 ++++++++---------- 6 files changed, 163 insertions(+), 123 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/account/Account.tsx b/server/sonar-web/src/main/js/apps/account/Account.tsx index 0cbb0cfade6..9af6a4ec10f 100644 --- a/server/sonar-web/src/main/js/apps/account/Account.tsx +++ b/server/sonar-web/src/main/js/apps/account/Account.tsx @@ -17,8 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { LargeCenteredLayout, PageContentFontWrapper, TopBar } from 'design-system'; import * as React from 'react'; +import { createPortal } from 'react-dom'; import { Helmet } from 'react-helmet-async'; import { Outlet } from 'react-router-dom'; import { useCurrentLoginUser } from '../../app/components/current-user/CurrentUserContext'; @@ -31,22 +33,35 @@ import UserCard from './components/UserCard'; export default function Account() { const currentUser = useCurrentLoginUser(); + const [portalAnchor, setPortalAnchor] = React.useState(null); + + // Set portal anchor on mount + React.useEffect(() => { + setPortalAnchor(document.getElementById('component-nav-portal')); + }, []); const title = translate('my_account.page'); + return (
-
- -
- -
-
+ {portalAnchor && + createPortal( +
+ +
+ +
+ +
, + portalAnchor, + )} + + + diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx index e3d1e1b39af..b9e31528a25 100644 --- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx +++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx @@ -20,6 +20,8 @@ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +import React from 'react'; +import { Outlet, Route } from 'react-router-dom'; import selectEvent from 'react-select-event'; import { getMyProjects, getScannableProjects } from '../../../api/components'; import NotificationsMock from '../../../api/mocks/NotificationsMock'; @@ -176,7 +178,7 @@ it('should render the top menu', () => { expect(screen.getByText(name)).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'my_account.profile' })).toBeInTheDocument(); + expect(screen.getByText('my_account.profile')).toBeInTheDocument(); expect(screen.getByText('my_account.security')).toBeInTheDocument(); expect(screen.getByText('my_account.notifications')).toBeInTheDocument(); expect(screen.getByText('my_account.projects')).toBeInTheDocument(); @@ -271,7 +273,7 @@ describe('security page', () => { securityPagePath, ); - expect(await screen.findByText('users.tokens')).toBeInTheDocument(); + expect(await screen.findByText('users.tokens.generate')).toBeInTheDocument(); await waitFor(() => expect(screen.getAllByRole('row')).toHaveLength(3)); // 2 tokens + header // Add the token @@ -372,7 +374,7 @@ describe('security page', () => { securityPagePath, ); - expect(await screen.findByText('users.tokens')).toBeInTheDocument(); + expect(await screen.findByText('users.tokens.generate')).toBeInTheDocument(); // expired token is flagged as such const expiredTokenRow = await screen.findByRole('row', { name: /expired token/ }); @@ -667,5 +669,22 @@ function getProjectBlock(projectName: string) { } function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) { - renderAppRoutes('account', routes, { currentUser, navigateTo }); + renderAppRoutes( + 'account', + () => ( + +
+ + + + } + > + {routes()} + + ), + { currentUser, navigateTo }, + ); } diff --git a/server/sonar-web/src/main/js/apps/account/security/Security.tsx b/server/sonar-web/src/main/js/apps/account/security/Security.tsx index 39f270f1818..fa7e912e9a2 100644 --- a/server/sonar-web/src/main/js/apps/account/security/Security.tsx +++ b/server/sonar-web/src/main/js/apps/account/security/Security.tsx @@ -17,6 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { PageTitle, SubHeading } from 'design-system'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { useCurrentLoginUser } from '../../../app/components/current-user/CurrentUserContext'; @@ -27,15 +29,21 @@ import Tokens from './Tokens'; export default function Security() { const currentUser = useCurrentLoginUser(); return ( -
+ <> + + {currentUser.local && ( -
-

{translate('my_profile.password.title')}

- -
+ + + + + )} -
+ ); } diff --git a/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx b/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx index 7c6804fc214..27d6728c85e 100644 --- a/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx +++ b/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx @@ -17,6 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { Title } from 'design-system'; import * as React from 'react'; import InstanceMessage from '../../../components/common/InstanceMessage'; import { translate } from '../../../helpers/l10n'; @@ -26,17 +28,18 @@ interface Props { login: string; } -export default function Tokens({ login }: Props) { +export default function Tokens({ login }: Readonly) { return ( -
-

{translate('users.tokens')}

-
-
+ <> + {translate('my_account.security')} + +
+
-
+ ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx index 539fc0feeed..6961767c107 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx @@ -25,6 +25,7 @@ import { InputField, InputSelect, Spinner, + SubHeading, Table, TableRow, } from 'design-system'; @@ -194,7 +195,9 @@ export function TokensForm(props: Readonly) { return ( <> -

{translate('users.tokens.generate')}

+ + + {translate('users.tokens.generate')}
@@ -284,14 +287,14 @@ export function TokensForm(props: Readonly) { - - + +
{tokens && tokens.length <= 0 ? ( @@ -308,8 +311,8 @@ export function TokensForm(props: Readonly) { /> )) )} - -
+ + ); } diff --git a/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx b/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx index 6a7b36061f4..b282209f32f 100644 --- a/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx +++ b/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx @@ -17,140 +17,130 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { ButtonPrimary, FlagMessage, FormField, InputField } from 'design-system'; import * as React from 'react'; import { changePassword } from '../../api/users'; -import { SubmitButton } from '../../components/controls/buttons'; -import { Alert } from '../../components/ui/Alert'; -import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker'; import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../helpers/l10n'; import { ChangePasswordResults, LoggedInUser } from '../../types/users'; interface Props { className?: string; - user: LoggedInUser; onPasswordChange?: () => void; + user: LoggedInUser; } -interface State { - errors?: string[]; - success: boolean; -} +export default function ResetPasswordForm({ + className, + onPasswordChange, + user: { login }, +}: Readonly) { + const [error, setError] = React.useState(undefined); + const [oldPassword, setOldPassword] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [passwordConfirmation, setPasswordConfirmation] = React.useState(''); + const [success, setSuccess] = React.useState(false); -export default class ResetPasswordForm extends React.Component { - oldPassword: HTMLInputElement | null = null; - password: HTMLInputElement | null = null; - passwordConfirmation: HTMLInputElement | null = null; - state: State = { - success: false, - }; + const handleSuccessfulChange = () => { + setOldPassword(''); + setPassword(''); + setPasswordConfirmation(''); + setSuccess(true); - handleSuccessfulChange = () => { - if (!this.oldPassword || !this.password || !this.passwordConfirmation) { - return; - } - this.oldPassword.value = ''; - this.password.value = ''; - this.passwordConfirmation.value = ''; - this.setState({ success: true, errors: undefined }); - if (this.props.onPasswordChange) { - this.props.onPasswordChange(); - } - }; - - setErrors = (errors: string[]) => { - this.setState({ success: false, errors }); + onPasswordChange?.(); }; - handleChangePassword = (event: React.FormEvent) => { + const handleChangePassword = (event: React.FormEvent) => { event.preventDefault(); - if (!this.oldPassword || !this.password || !this.passwordConfirmation) { - return; - } - const { user } = this.props; - const previousPassword = this.oldPassword.value; - const password = this.password.value; - const passwordConfirmation = this.passwordConfirmation.value; + + setError(undefined); + setSuccess(false); if (password !== passwordConfirmation) { - this.password.focus(); - this.setErrors([translate('user.password_doesnt_match_confirmation')]); + setError(translate('user.password_doesnt_match_confirmation')); } else { - changePassword({ login: user.login, password, previousPassword }) - .then(this.handleSuccessfulChange) + changePassword({ login, password, previousPassword: oldPassword }) + .then(handleSuccessfulChange) .catch((result: ChangePasswordResults) => { if (result === ChangePasswordResults.OldPasswordIncorrect) { - this.setErrors([translate('user.old_password_incorrect')]); + setError(translate('user.old_password_incorrect')); } else if (result === ChangePasswordResults.NewPasswordSameAsOld) { - this.setErrors([translate('user.new_password_same_as_old')]); + setError(translate('user.new_password_same_as_old')); } }); } }; - render() { - const { className } = this.props; - const { success, errors } = this.state; - - return ( - - {success && {translate('my_profile.password.changed')}} + return ( + + {success && ( +
+ {translate('my_profile.password.changed')} +
+ )} - {errors && - errors.map((e) => ( - - {e} - - ))} + {error !== undefined && ( +
+ {error} +
+ )} - + -
- - + + (this.oldPassword = elem)} + onChange={(e: React.ChangeEvent) => setOldPassword(e.target.value)} required type="password" + value={oldPassword} /> -
-
- - +
+ +
+ + (this.password = elem)} + onChange={(e: React.ChangeEvent) => setPassword(e.target.value)} required type="password" + value={password} /> -
-
- - +
+ +
+ + (this.passwordConfirmation = elem)} + onChange={(e: React.ChangeEvent) => + setPasswordConfirmation(e.target.value) + } required type="password" + value={passwordConfirmation} /> -
-
- {translate('update_verb')} -
- - ); - } + +
+ +
+ + {translate('update_verb')} + +
+ + ); } -- 2.39.5