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