mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Login: Show error messages inline in form instead of in toasts (#70266)
* Login: Show error messages inline in form instead of in toasts * remove console.log lol * fix test
This commit is contained in:
parent
31b9f9d235
commit
e9d42a6395
@ -1,9 +1,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { FetchError, getBackendSrv, isFetchError } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import config from 'app/core/config';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { LoginDTO } from './types';
|
||||
|
||||
@ -32,6 +33,7 @@ interface Props {
|
||||
loginHint: string;
|
||||
passwordHint: string;
|
||||
showDefaultPasswordWarning: boolean;
|
||||
loginErrorMessage: string | undefined;
|
||||
}) => JSX.Element;
|
||||
}
|
||||
|
||||
@ -39,6 +41,7 @@ interface State {
|
||||
isLoggingIn: boolean;
|
||||
isChangingPassword: boolean;
|
||||
showDefaultPasswordWarning: boolean;
|
||||
loginErrorMessage?: string;
|
||||
}
|
||||
|
||||
export class LoginCtrl extends PureComponent<Props, State> {
|
||||
@ -88,11 +91,12 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
||||
|
||||
login = (formModel: FormModel) => {
|
||||
this.setState({
|
||||
loginErrorMessage: undefined,
|
||||
isLoggingIn: true,
|
||||
});
|
||||
|
||||
getBackendSrv()
|
||||
.post<LoginDTO>('/login', formModel)
|
||||
.post<LoginDTO>('/login', formModel, { showErrorAlert: false })
|
||||
.then((result) => {
|
||||
this.result = result;
|
||||
if (formModel.password !== 'admin' || config.ldapEnabled || config.authProxyEnabled) {
|
||||
@ -102,9 +106,11 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
||||
this.changeView(formModel.password === 'admin');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
const fetchErrorMessage = isFetchError(err) ? getErrorMessage(err) : undefined;
|
||||
this.setState({
|
||||
isLoggingIn: false,
|
||||
loginErrorMessage: fetchErrorMessage || t('login.error.unknown', 'Unknown error occurred'),
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -131,7 +137,7 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning } = this.state;
|
||||
const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage } = this.state;
|
||||
const { login, toGrafana, changePassword } = this;
|
||||
const { loginHint, passwordHint, disableLoginForm, disableUserSignUp } = config;
|
||||
|
||||
@ -149,6 +155,7 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
||||
skipPasswordChange: toGrafana,
|
||||
isChangingPassword,
|
||||
showDefaultPasswordWarning,
|
||||
loginErrorMessage,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
@ -156,3 +163,15 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
export default LoginCtrl;
|
||||
|
||||
function getErrorMessage(err: FetchError<undefined | { messageId?: string; message?: string }>): string | undefined {
|
||||
switch (err.data?.messageId) {
|
||||
case 'password-auth.empty':
|
||||
case 'password-auth.failed':
|
||||
case 'password-auth.invalid':
|
||||
case 'login-attempt.blocked':
|
||||
return t('login.error.invalid-user-or-password', 'Invalid username or password');
|
||||
default:
|
||||
return err.data?.message;
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ describe('Login Page', () => {
|
||||
expect(screen.getByRole('link', { name: 'Sign up' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Sign up' })).toHaveAttribute('href', '/signup');
|
||||
});
|
||||
|
||||
it('should pass validation checks for username field', async () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
@ -60,6 +61,7 @@ describe('Login Page', () => {
|
||||
await userEvent.type(screen.getByRole('textbox', { name: 'Username input field' }), 'admin');
|
||||
await waitFor(() => expect(screen.queryByText('Email or username is required')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should pass validation checks for password field', async () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
@ -69,6 +71,7 @@ describe('Login Page', () => {
|
||||
await userEvent.type(screen.getByLabelText('Password input field'), 'admin');
|
||||
await waitFor(() => expect(screen.queryByText('Password is required')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should navigate to default url if credentials is valid', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
@ -82,9 +85,12 @@ describe('Login Page', () => {
|
||||
await userEvent.type(screen.getByLabelText('Password input field'), 'test');
|
||||
fireEvent.click(screen.getByLabelText('Login button'));
|
||||
|
||||
await waitFor(() => expect(postMock).toHaveBeenCalledWith('/login', { password: 'test', user: 'admin' }));
|
||||
await waitFor(() =>
|
||||
expect(postMock).toHaveBeenCalledWith('/login', { password: 'test', user: 'admin' }, { showErrorAlert: false })
|
||||
);
|
||||
expect(window.location.assign).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
it('renders social logins correctly', () => {
|
||||
runtimeMock.config.oauth = {
|
||||
okta: {
|
||||
|
@ -3,7 +3,7 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
// Components
|
||||
import { HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
import { Alert, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
import { Branding } from 'app/core/components/Branding/Branding';
|
||||
import config from 'app/core/config';
|
||||
|
||||
@ -20,6 +20,10 @@ const forgottenPasswordStyles = css`
|
||||
margin-top: 4px;
|
||||
`;
|
||||
|
||||
const alertStyles = css({
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const LoginPage = () => {
|
||||
document.title = Branding.AppTitle;
|
||||
return (
|
||||
@ -35,10 +39,13 @@ export const LoginPage = () => {
|
||||
skipPasswordChange,
|
||||
isChangingPassword,
|
||||
showDefaultPasswordWarning,
|
||||
loginErrorMessage,
|
||||
}) => (
|
||||
<LoginLayout isChangingPassword={isChangingPassword}>
|
||||
{!isChangingPassword && (
|
||||
<InnerBox>
|
||||
{loginErrorMessage && <Alert className={alertStyles} severity="error" title={loginErrorMessage} />}
|
||||
|
||||
{!disableLoginForm && (
|
||||
<LoginForm onSubmit={login} loginHint={loginHint} passwordHint={passwordHint} isLoggingIn={isLoggingIn}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
@ -56,6 +63,7 @@ export const LoginPage = () => {
|
||||
{!disableUserSignUp && <UserSignup />}
|
||||
</InnerBox>
|
||||
)}
|
||||
|
||||
{isChangingPassword && (
|
||||
<InnerBox>
|
||||
<ChangePassword
|
||||
|
@ -163,6 +163,12 @@
|
||||
"success": "Bibliotheks-Panel wurde gespeichert"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"invalid-user-or-password": "",
|
||||
"unknown": ""
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"admin": {
|
||||
"subtitle": "Serverweite Einstellungen und Zugriff auf Ressourcen wie Organisationen, Benutzer und Lizenzen verwalten",
|
||||
|
@ -163,6 +163,12 @@
|
||||
"success": "Library panel saved"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"invalid-user-or-password": "Invalid username or password",
|
||||
"unknown": "Unknown error occurred"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"admin": {
|
||||
"subtitle": "Manage server-wide settings and access to resources such as organizations, users, and licenses",
|
||||
|
@ -163,6 +163,12 @@
|
||||
"success": "Panel de librería guardado"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"invalid-user-or-password": "",
|
||||
"unknown": ""
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"admin": {
|
||||
"subtitle": "Administrar la configuración de todo el servidor y el acceso a recursos como organizaciones, usuarios y licencias",
|
||||
|
@ -163,6 +163,12 @@
|
||||
"success": "Panneau de bibliothèque enregistré"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"invalid-user-or-password": "",
|
||||
"unknown": ""
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"admin": {
|
||||
"subtitle": "Gérer les paramètres à l'échelle du serveur et l'accès aux ressources telles que les organisations, les utilisateurs et les licences",
|
||||
|
@ -163,6 +163,12 @@
|
||||
"success": "Ŀįþřäřy päʼnęľ şävęđ"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"invalid-user-or-password": "Ĩʼnväľįđ ūşęřʼnämę őř päşşŵőřđ",
|
||||
"unknown": "Ůʼnĸʼnőŵʼn ęřřőř őččūřřęđ"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"admin": {
|
||||
"subtitle": "Mäʼnäģę şęřvęř-ŵįđę şęŧŧįʼnģş äʼnđ äččęşş ŧő řęşőūřčęş şūčĥ äş őřģäʼnįžäŧįőʼnş, ūşęřş, äʼnđ ľįčęʼnşęş",
|
||||
|
@ -163,6 +163,12 @@
|
||||
"success": "库面板已保存"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"invalid-user-or-password": "",
|
||||
"unknown": ""
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"admin": {
|
||||
"subtitle": "管理整个服务器范围的设置,以及对组织、用户和许可证等资源的访问权限",
|
||||
|
Loading…
Reference in New Issue
Block a user