mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Login: Angular to React (#18116)
* Migrating login services * Add user signup * Remove lodash * Remove media query and extarct LoginServices * Add React LoginCtrl * Handle location with Redux and start form validation * Fix proposal * Add basic validation * Fix validation * Remove state from controller * Extract login forms * Fix things up * Add change password and LoginPage * Add React page and route to it * Make redux connection work * Add validation for password change * Change pws request * Fix feedback * Fix feedback * LoginPage to FC * Move onSkip to a method * Experimenting with animations * Make animations work * Add input focus * Fix focus problem and clean animation * Working change password request * Add routing with window.location instead of Redux * Fix a bit of feedback * Move config to LoginCtrl * Make buttons same size * Change way of validating * Update changePassword and remove angular controller * Remove some console.logs * Split onChange * Remove className * Fix animation, onChange and remove config.loginError code * Add loginError appEvent * Make flex and add previosuly removed media query
This commit is contained in:
parent
93ecf63e70
commit
91a911b64e
135
public/app/core/components/Login/ChangePassword.tsx
Normal file
135
public/app/core/components/Login/ChangePassword.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { PureComponent, SyntheticEvent, ChangeEvent } from 'react';
|
||||||
|
import { Tooltip } from '@grafana/ui';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSubmit: (pw: string) => void;
|
||||||
|
onSkip: Function;
|
||||||
|
focus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
newPassword: string;
|
||||||
|
confirmNew: string;
|
||||||
|
valid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChangePassword extends PureComponent<Props, State> {
|
||||||
|
private userInput: HTMLInputElement;
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
newPassword: '',
|
||||||
|
confirmNew: '',
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (!prevProps.focus && this.props.focus) {
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.userInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = (e: SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { newPassword, valid } = this.state;
|
||||||
|
if (valid) {
|
||||||
|
this.props.onSubmit(newPassword);
|
||||||
|
} else {
|
||||||
|
appEvents.emit('alert-warning', ['New passwords do not match', '']);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onNewPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({
|
||||||
|
newPassword: e.target.value,
|
||||||
|
valid: this.validate('newPassword', e.target.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({
|
||||||
|
confirmNew: e.target.value,
|
||||||
|
valid: this.validate('confirmNew', e.target.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSkip = (e: SyntheticEvent) => {
|
||||||
|
this.props.onSkip();
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(changed: string, pw: string) {
|
||||||
|
if (changed === 'newPassword') {
|
||||||
|
return this.state.confirmNew === pw;
|
||||||
|
} else if (changed === 'confirmNew') {
|
||||||
|
return this.state.newPassword === pw;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="login-inner-box" id="change-password-view">
|
||||||
|
<div className="text-left login-change-password-info">
|
||||||
|
<h5>Change Password</h5>
|
||||||
|
Before you can get started with awesome dashboards we need you to make your account more secure by changing
|
||||||
|
your password.
|
||||||
|
<br />
|
||||||
|
You can change your password again later.
|
||||||
|
</div>
|
||||||
|
<form className="login-form-group gf-form-group">
|
||||||
|
<div className="login-form">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
className="gf-form-input login-form-input"
|
||||||
|
required
|
||||||
|
placeholder="New password"
|
||||||
|
onChange={this.onNewPasswordChange}
|
||||||
|
ref={input => {
|
||||||
|
this.userInput = input;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="login-form">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmNew"
|
||||||
|
className="gf-form-input login-form-input"
|
||||||
|
required
|
||||||
|
ng-model="command.confirmNew"
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
onChange={this.onConfirmPasswordChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="login-button-group login-button-group--right text-right">
|
||||||
|
<Tooltip
|
||||||
|
placement="bottom"
|
||||||
|
content="If you skip you will be prompted to change password next time you login."
|
||||||
|
>
|
||||||
|
<a className="btn btn-link" onClick={this.onSkip}>
|
||||||
|
Skip
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`btn btn-large p-x-2 ${this.state.valid ? 'btn-primary' : 'btn-inverse'}`}
|
||||||
|
onClick={this.onSubmit}
|
||||||
|
disabled={!this.state.valid}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
162
public/app/core/components/Login/LoginCtrl.tsx
Normal file
162
public/app/core/components/Login/LoginCtrl.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
|
const isOauthEnabled = () => Object.keys(config.oauth).length > 0;
|
||||||
|
|
||||||
|
export interface FormModel {
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
routeParams?: any;
|
||||||
|
updateLocation?: typeof updateLocation;
|
||||||
|
children: (props: {
|
||||||
|
isLoggingIn: boolean;
|
||||||
|
changePassword: (pw: string) => void;
|
||||||
|
isChangingPassword: boolean;
|
||||||
|
skipPasswordChange: Function;
|
||||||
|
login: (data: FormModel) => void;
|
||||||
|
disableLoginForm: boolean;
|
||||||
|
ldapEnabled: boolean;
|
||||||
|
authProxyEnabled: boolean;
|
||||||
|
disableUserSignUp: boolean;
|
||||||
|
isOauthEnabled: boolean;
|
||||||
|
loginHint: string;
|
||||||
|
passwordHint: string;
|
||||||
|
}) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isLoggingIn: boolean;
|
||||||
|
isChangingPassword: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginCtrl extends PureComponent<Props, State> {
|
||||||
|
result: any = {};
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isLoggingIn: false,
|
||||||
|
isChangingPassword: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.loginError) {
|
||||||
|
appEvents.emit('alert-warning', ['Login Failed', config.loginError]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changePassword = (password: string) => {
|
||||||
|
const pw = {
|
||||||
|
newPassword: password,
|
||||||
|
confirmNew: password,
|
||||||
|
oldPassword: 'admin',
|
||||||
|
};
|
||||||
|
getBackendSrv()
|
||||||
|
.put('/api/user/password', pw)
|
||||||
|
.then(() => {
|
||||||
|
this.toGrafana();
|
||||||
|
})
|
||||||
|
.catch((err: any) => console.log(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
login = (formModel: FormModel) => {
|
||||||
|
this.setState({
|
||||||
|
isLoggingIn: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
getBackendSrv()
|
||||||
|
.post('/login', formModel)
|
||||||
|
.then((result: any) => {
|
||||||
|
this.result = result;
|
||||||
|
if (formModel.password !== 'admin' || config.ldapEnabled || config.authProxyEnabled) {
|
||||||
|
this.toGrafana();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.changeView();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({
|
||||||
|
isLoggingIn: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
changeView = () => {
|
||||||
|
this.setState({
|
||||||
|
isChangingPassword: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toGrafana = () => {
|
||||||
|
const params = this.props.routeParams;
|
||||||
|
// Use window.location.href to force page reload
|
||||||
|
if (params.redirect && params.redirect[0] === '/') {
|
||||||
|
window.location.href = config.appSubUrl + params.redirect;
|
||||||
|
|
||||||
|
// this.props.updateLocation({
|
||||||
|
// path: config.appSubUrl + params.redirect,
|
||||||
|
// });
|
||||||
|
} else if (this.result.redirectUrl) {
|
||||||
|
window.location.href = config.appSubUrl + params.redirect;
|
||||||
|
|
||||||
|
// this.props.updateLocation({
|
||||||
|
// path: this.result.redirectUrl,
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
window.location.href = config.appSubUrl + '/';
|
||||||
|
|
||||||
|
// this.props.updateLocation({
|
||||||
|
// path: '/',
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
const { isLoggingIn, isChangingPassword } = this.state;
|
||||||
|
const { login, toGrafana, changePassword } = this;
|
||||||
|
const { loginHint, passwordHint, disableLoginForm, ldapEnabled, authProxyEnabled, disableUserSignUp } = config;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children({
|
||||||
|
isOauthEnabled: isOauthEnabled(),
|
||||||
|
loginHint,
|
||||||
|
passwordHint,
|
||||||
|
disableLoginForm,
|
||||||
|
ldapEnabled,
|
||||||
|
authProxyEnabled,
|
||||||
|
disableUserSignUp,
|
||||||
|
login,
|
||||||
|
isLoggingIn,
|
||||||
|
changePassword,
|
||||||
|
skipPasswordChange: toGrafana,
|
||||||
|
isChangingPassword,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapStateToProps = (state: StoreState) => ({
|
||||||
|
routeParams: state.location.routeParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = { updateLocation };
|
||||||
|
|
||||||
|
export default hot(module)(
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(LoginCtrl)
|
||||||
|
);
|
120
public/app/core/components/Login/LoginForm.tsx
Normal file
120
public/app/core/components/Login/LoginForm.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { PureComponent, SyntheticEvent, ChangeEvent } from 'react';
|
||||||
|
import { FormModel } from './LoginCtrl';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
displayForgotPassword: boolean;
|
||||||
|
onChange?: (valid: boolean) => void;
|
||||||
|
onSubmit: (data: FormModel) => void;
|
||||||
|
isLoggingIn: boolean;
|
||||||
|
passwordHint: string;
|
||||||
|
loginHint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
email: string;
|
||||||
|
valid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginForm extends PureComponent<Props, State> {
|
||||||
|
private userInput: HTMLInputElement;
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
user: '',
|
||||||
|
password: '',
|
||||||
|
email: '',
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.userInput.focus();
|
||||||
|
}
|
||||||
|
onSubmit = (e: SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { user, password, email } = this.state;
|
||||||
|
if (this.state.valid) {
|
||||||
|
this.props.onSubmit({ user, password, email });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangePassword = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({
|
||||||
|
password: e.target.value,
|
||||||
|
valid: this.validate(this.state.user, e.target.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeUsername = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({
|
||||||
|
user: e.target.value,
|
||||||
|
valid: this.validate(e.target.value, this.state.password),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(user: string, password: string) {
|
||||||
|
return user.length > 0 && password.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<form name="loginForm" className="login-form-group gf-form-group">
|
||||||
|
<div className="login-form">
|
||||||
|
<input
|
||||||
|
ref={input => {
|
||||||
|
this.userInput = input;
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
name="user"
|
||||||
|
className="gf-form-input login-form-input"
|
||||||
|
required
|
||||||
|
placeholder={this.props.loginHint}
|
||||||
|
aria-label="Username input field"
|
||||||
|
onChange={this.onChangeUsername}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="login-form">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
className="gf-form-input login-form-input"
|
||||||
|
required
|
||||||
|
ng-model="formModel.password"
|
||||||
|
id="inputPassword"
|
||||||
|
placeholder={this.props.passwordHint}
|
||||||
|
aria-label="Password input field"
|
||||||
|
onChange={this.onChangePassword}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="login-button-group">
|
||||||
|
{!this.props.isLoggingIn ? (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
aria-label="Login button"
|
||||||
|
className={`btn btn-large p-x-2 ${this.state.valid ? 'btn-primary' : 'btn-inverse'}`}
|
||||||
|
onClick={this.onSubmit}
|
||||||
|
disabled={!this.state.valid}
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="submit" className="btn btn-large p-x-2 btn-inverse btn-loading">
|
||||||
|
Logging In<span>.</span>
|
||||||
|
<span>.</span>
|
||||||
|
<span>.</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.props.displayForgotPassword ? (
|
||||||
|
<div className="small login-button-forgot-password">
|
||||||
|
<a href="user/password/send-reset-email">Forgot your password?</a>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
81
public/app/core/components/Login/LoginPage.tsx
Normal file
81
public/app/core/components/Login/LoginPage.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { UserSignup } from './UserSignup';
|
||||||
|
import { LoginServiceButtons } from './LoginServiceButtons';
|
||||||
|
import LoginCtrl from './LoginCtrl';
|
||||||
|
import { LoginForm } from './LoginForm';
|
||||||
|
import { ChangePassword } from './ChangePassword';
|
||||||
|
import { CSSTransition } from 'react-transition-group';
|
||||||
|
|
||||||
|
export const LoginPage: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="login container">
|
||||||
|
<div className="login-content">
|
||||||
|
<div className="login-branding">
|
||||||
|
<img className="logo-icon" src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||||
|
<div className="logo-wordmark" />
|
||||||
|
</div>
|
||||||
|
<LoginCtrl>
|
||||||
|
{({
|
||||||
|
loginHint,
|
||||||
|
passwordHint,
|
||||||
|
isOauthEnabled,
|
||||||
|
ldapEnabled,
|
||||||
|
authProxyEnabled,
|
||||||
|
disableLoginForm,
|
||||||
|
disableUserSignUp,
|
||||||
|
login,
|
||||||
|
isLoggingIn,
|
||||||
|
changePassword,
|
||||||
|
skipPasswordChange,
|
||||||
|
isChangingPassword,
|
||||||
|
}) => (
|
||||||
|
<div className="login-outer-box">
|
||||||
|
<div className={`login-inner-box ${isChangingPassword ? 'hidden' : ''}`} id="login-view">
|
||||||
|
{!disableLoginForm ? (
|
||||||
|
<LoginForm
|
||||||
|
displayForgotPassword={!(ldapEnabled || authProxyEnabled)}
|
||||||
|
onSubmit={login}
|
||||||
|
loginHint={loginHint}
|
||||||
|
passwordHint={passwordHint}
|
||||||
|
isLoggingIn={isLoggingIn}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isOauthEnabled ? (
|
||||||
|
<>
|
||||||
|
<div className="text-center login-divider">
|
||||||
|
<div>
|
||||||
|
<div className="login-divider-line" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="login-divider-text">{disableLoginForm ? null : <span>or</span>}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="login-divider-line" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="clearfix" />
|
||||||
|
|
||||||
|
<LoginServiceButtons />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{!disableUserSignUp ? <UserSignup /> : null}
|
||||||
|
</div>
|
||||||
|
<CSSTransition
|
||||||
|
appear={true}
|
||||||
|
mountOnEnter={true}
|
||||||
|
in={isChangingPassword}
|
||||||
|
timeout={250}
|
||||||
|
classNames="login-inner-box"
|
||||||
|
>
|
||||||
|
<ChangePassword onSubmit={changePassword} onSkip={skipPasswordChange} focus={isChangingPassword} />
|
||||||
|
</CSSTransition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</LoginCtrl>
|
||||||
|
|
||||||
|
<div className="clearfix" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
67
public/app/core/components/Login/LoginServiceButtons.tsx
Normal file
67
public/app/core/components/Login/LoginServiceButtons.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
|
const loginServices: () => LoginServices = () => ({
|
||||||
|
saml: {
|
||||||
|
enabled: config.samlEnabled,
|
||||||
|
name: 'SAML',
|
||||||
|
className: 'github',
|
||||||
|
icon: 'key',
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
enabled: config.oauth.google,
|
||||||
|
name: 'Google',
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
enabled: config.oauth.github,
|
||||||
|
name: 'GitHub',
|
||||||
|
},
|
||||||
|
gitlab: {
|
||||||
|
enabled: config.oauth.gitlab,
|
||||||
|
name: 'GitLab',
|
||||||
|
},
|
||||||
|
grafanacom: {
|
||||||
|
enabled: config.oauth.grafana_com,
|
||||||
|
name: 'Grafana.com',
|
||||||
|
hrefName: 'grafana_com',
|
||||||
|
icon: 'grafana_com',
|
||||||
|
},
|
||||||
|
oauth: {
|
||||||
|
enabled: config.oauth.generic_oauth,
|
||||||
|
name: 'OAuth',
|
||||||
|
icon: 'sign-in',
|
||||||
|
hrefName: 'generic_oauth',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface LoginService {
|
||||||
|
enabled: boolean;
|
||||||
|
name: string;
|
||||||
|
hrefName?: string;
|
||||||
|
icon?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginServices {
|
||||||
|
[key: string]: LoginService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginServiceButtons = () => {
|
||||||
|
const keyNames = Object.keys(loginServices());
|
||||||
|
const serviceElements = keyNames.map(key => {
|
||||||
|
const service: LoginService = loginServices()[key];
|
||||||
|
return service.enabled ? (
|
||||||
|
<a
|
||||||
|
key={key}
|
||||||
|
className={`btn btn-medium btn-service btn-service--${service.className || key} login-btn`}
|
||||||
|
href={`login/${service.hrefName ? service.hrefName : key}`}
|
||||||
|
target="_self"
|
||||||
|
>
|
||||||
|
<i className={`btn-service-icon fa fa-${service.icon ? service.icon : key}`} />
|
||||||
|
Sign in with {service.name}
|
||||||
|
</a>
|
||||||
|
) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="login-oauth text-center">{serviceElements}</div>;
|
||||||
|
};
|
12
public/app/core/components/Login/UserSignup.tsx
Normal file
12
public/app/core/components/Login/UserSignup.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
export const UserSignup: FC<{}> = () => {
|
||||||
|
return (
|
||||||
|
<div className="login-signup-box">
|
||||||
|
<div className="login-signup-title p-r-1">New to Grafana?</div>
|
||||||
|
<a href="signup" className="btn btn-medium btn-signup btn-p-x-2">
|
||||||
|
Sign Up
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import './json_editor_ctrl';
|
import './json_editor_ctrl';
|
||||||
import './login_ctrl';
|
|
||||||
import './invited_ctrl';
|
import './invited_ctrl';
|
||||||
import './signup_ctrl';
|
import './signup_ctrl';
|
||||||
import './reset_password_ctrl';
|
import './reset_password_ctrl';
|
||||||
|
@ -1,145 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import coreModule from '../core_module';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { BackendSrv } from '../services/backend_srv';
|
|
||||||
|
|
||||||
export class LoginCtrl {
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope: any, backendSrv: BackendSrv, $location: any) {
|
|
||||||
$scope.formModel = {
|
|
||||||
user: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.command = {};
|
|
||||||
$scope.result = '';
|
|
||||||
$scope.loggingIn = false;
|
|
||||||
|
|
||||||
$scope.oauth = config.oauth;
|
|
||||||
$scope.oauthEnabled = _.keys(config.oauth).length > 0;
|
|
||||||
$scope.ldapEnabled = config.ldapEnabled;
|
|
||||||
$scope.authProxyEnabled = config.authProxyEnabled;
|
|
||||||
$scope.samlEnabled = config.samlEnabled;
|
|
||||||
|
|
||||||
$scope.disableLoginForm = config.disableLoginForm;
|
|
||||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
|
||||||
$scope.loginHint = config.loginHint;
|
|
||||||
$scope.passwordHint = config.passwordHint;
|
|
||||||
|
|
||||||
$scope.loginMode = true;
|
|
||||||
$scope.submitBtnText = 'Log in';
|
|
||||||
|
|
||||||
$scope.init = () => {
|
|
||||||
$scope.$watch('loginMode', $scope.loginModeChanged);
|
|
||||||
|
|
||||||
if (config.loginError) {
|
|
||||||
$scope.appEvent('alert-warning', ['Login Failed', config.loginError]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.submit = () => {
|
|
||||||
if ($scope.loginMode) {
|
|
||||||
$scope.login();
|
|
||||||
} else {
|
|
||||||
$scope.signUp();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.changeView = () => {
|
|
||||||
const loginView = document.querySelector('#login-view');
|
|
||||||
const changePasswordView = document.querySelector('#change-password-view');
|
|
||||||
|
|
||||||
loginView.className += ' add';
|
|
||||||
setTimeout(() => {
|
|
||||||
loginView.className += ' hidden';
|
|
||||||
}, 250);
|
|
||||||
setTimeout(() => {
|
|
||||||
changePasswordView.classList.remove('hidden');
|
|
||||||
}, 251);
|
|
||||||
setTimeout(() => {
|
|
||||||
changePasswordView.classList.remove('remove');
|
|
||||||
}, 301);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.getElementById('newPassword').focus();
|
|
||||||
}, 400);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.changePassword = () => {
|
|
||||||
$scope.command.oldPassword = 'admin';
|
|
||||||
|
|
||||||
if ($scope.command.newPassword !== $scope.command.confirmNew) {
|
|
||||||
$scope.appEvent('alert-warning', ['New passwords do not match', '']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
backendSrv.put('/api/user/password', $scope.command).then(() => {
|
|
||||||
$scope.toGrafana();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.skip = () => {
|
|
||||||
$scope.toGrafana();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.loginModeChanged = (newValue: boolean) => {
|
|
||||||
$scope.submitBtnText = newValue ? 'Log in' : 'Sign up';
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.signUp = () => {
|
|
||||||
if (!$scope.loginForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
backendSrv.post('/api/user/signup', $scope.formModel).then((result: any) => {
|
|
||||||
if (result.status === 'SignUpCreated') {
|
|
||||||
$location.path('/signup').search({ email: $scope.formModel.email });
|
|
||||||
} else {
|
|
||||||
window.location.href = config.appSubUrl + '/';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.login = () => {
|
|
||||||
delete $scope.loginError;
|
|
||||||
|
|
||||||
if (!$scope.loginForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$scope.loggingIn = true;
|
|
||||||
|
|
||||||
backendSrv
|
|
||||||
.post('/login', $scope.formModel)
|
|
||||||
.then((result: any) => {
|
|
||||||
$scope.result = result;
|
|
||||||
|
|
||||||
if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) {
|
|
||||||
$scope.toGrafana();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
$scope.changeView();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
$scope.loggingIn = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.toGrafana = () => {
|
|
||||||
const params = $location.search();
|
|
||||||
|
|
||||||
if (params.redirect && params.redirect[0] === '/') {
|
|
||||||
window.location.href = config.appSubUrl + params.redirect;
|
|
||||||
} else if ($scope.result.redirectUrl) {
|
|
||||||
window.location.href = $scope.result.redirectUrl;
|
|
||||||
} else {
|
|
||||||
window.location.href = config.appSubUrl + '/';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('LoginCtrl', LoginCtrl);
|
|
@ -1,115 +0,0 @@
|
|||||||
<div class="login container">
|
|
||||||
<div class="login-content">
|
|
||||||
<div class="login-branding">
|
|
||||||
<img class="logo-icon" src="public/img/grafana_icon.svg" alt="Grafana" />
|
|
||||||
<div class="logo-wordmark" />
|
|
||||||
</div>
|
|
||||||
<div class="login-outer-box">
|
|
||||||
<div class="login-inner-box" id="login-view">
|
|
||||||
<form name="loginForm" class="login-form-group gf-form-group" ng-hide="disableLoginForm">
|
|
||||||
<div class="login-form">
|
|
||||||
<input type="text" name="username" class="gf-form-input login-form-input" required ng-model='formModel.user' placeholder={{loginHint}} aria-label="Username input field"
|
|
||||||
autofocus autofill-event-fix>
|
|
||||||
</div>
|
|
||||||
<div class="login-form">
|
|
||||||
<input type="password" name="password" class="gf-form-input login-form-input" required ng-model="formModel.password" id="inputPassword"
|
|
||||||
placeholder="{{passwordHint}}" aria-label="Password input field">
|
|
||||||
</div>
|
|
||||||
<div class="login-button-group">
|
|
||||||
<button type="submit" aria-label="Login button" class="btn btn-large p-x-2" ng-if="!loggingIn" ng-click="submit();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
|
|
||||||
Log In
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-large p-x-2 btn-inverse btn-loading" ng-if="loggingIn">
|
|
||||||
Logging In<span>.</span><span>.</span><span>.</span>
|
|
||||||
</button>
|
|
||||||
<div class="small login-button-forgot-password" ng-hide="ldapEnabled || authProxyEnabled">
|
|
||||||
<a href="user/password/send-reset-email">
|
|
||||||
Forgot your password?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="text-center login-divider" ng-show="oauthEnabled">
|
|
||||||
<div>
|
|
||||||
<div class="login-divider-line">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="login-divider-text">
|
|
||||||
<span ng-hide="disableLoginForm">or</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="login-divider-line">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
<a class="btn btn-medium btn-service btn-service--github login-btn" href="login/saml" target="_self" ng-if="samlEnabled">
|
|
||||||
<i class="btn-service-icon fa fa-key"></i>
|
|
||||||
Sign in with SAML
|
|
||||||
</a>
|
|
||||||
<div class="login-oauth text-center" ng-show="oauthEnabled">
|
|
||||||
<a class="btn btn-medium btn-service btn-service--google login-btn" href="login/google" target="_self" ng-if="oauth.google">
|
|
||||||
<i class="btn-service-icon fa fa-google"></i>
|
|
||||||
Sign in with Google
|
|
||||||
</a>
|
|
||||||
<a class="btn btn-medium btn-service btn-service--github login-btn" href="login/github" target="_self" ng-if="oauth.github">
|
|
||||||
<i class="btn-service-icon fa fa-github"></i>
|
|
||||||
Sign in with GitHub
|
|
||||||
</a>
|
|
||||||
<a class="btn btn-medium btn-service btn-service--gitlab login-btn" href="login/gitlab" target="_self" ng-if="oauth.gitlab">
|
|
||||||
<i class="btn-service-icon fa fa-gitlab"></i>
|
|
||||||
Sign in with GitLab
|
|
||||||
</a>
|
|
||||||
<a class="btn btn-medium btn-service btn-service--grafanacom login-btn" href="login/grafana_com" target="_self"
|
|
||||||
ng-if="oauth.grafana_com">
|
|
||||||
<i class="btn-service-icon"></i>
|
|
||||||
Sign in with Grafana.com
|
|
||||||
</a>
|
|
||||||
<a class="btn btn-medium btn-service btn-service--oauth login-btn" href="login/generic_oauth" target="_self"
|
|
||||||
ng-if="oauth.generic_oauth">
|
|
||||||
<i class="btn-service-icon fa fa-sign-in"></i>
|
|
||||||
Sign in with {{oauth.generic_oauth.name}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="login-signup-box" ng-show="!disableUserSignUp">
|
|
||||||
<div class="login-signup-title p-r-1">
|
|
||||||
New to Grafana?
|
|
||||||
</div>
|
|
||||||
<a href="signup" class="btn btn-medium btn-signup btn-p-x-2">
|
|
||||||
Sign Up
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="login-inner-box remove hidden" id="change-password-view">
|
|
||||||
<div class="text-left login-change-password-info">
|
|
||||||
<h5>Change Password</h5>
|
|
||||||
Before you can get started with awesome dashboards we need you to make your account more secure by changing your password.
|
|
||||||
<br />You can change your password again later.
|
|
||||||
</div>
|
|
||||||
<form class="login-form-group gf-form-group">
|
|
||||||
<div class="login-form">
|
|
||||||
<input type="password" id="newPassword" name="newPassword" class="gf-form-input login-form-input" required ng-model='command.newPassword'
|
|
||||||
placeholder="New password">
|
|
||||||
</div>
|
|
||||||
<div class="login-form">
|
|
||||||
<input type="password" name="confirmNew" class="gf-form-input login-form-input" required ng-model="command.confirmNew" placeholder="Confirm new password">
|
|
||||||
</div>
|
|
||||||
<div class="login-button-group login-button-group--right text-right">
|
|
||||||
<a class="btn btn-link" ng-click="skip();">
|
|
||||||
Skip
|
|
||||||
<info-popover mode="small-padding">
|
|
||||||
If you skip you will be prompted to change password next time you login.
|
|
||||||
</info-popover>
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-large p-x-2" ng-click="changePassword();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -30,6 +30,7 @@ import { route, ILocationProvider } from 'angular';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DashboardRouteInfo } from 'app/types';
|
import { DashboardRouteInfo } from 'app/types';
|
||||||
|
import { LoginPage } from 'app/core/components/Login/LoginPage';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) {
|
export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) {
|
||||||
@ -285,8 +286,10 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
|||||||
})
|
})
|
||||||
// LOGIN / SIGNUP
|
// LOGIN / SIGNUP
|
||||||
.when('/login', {
|
.when('/login', {
|
||||||
templateUrl: 'public/app/partials/login.html',
|
template: '<react-container/>',
|
||||||
controller: 'LoginCtrl',
|
resolve: {
|
||||||
|
component: () => LoginPage,
|
||||||
|
},
|
||||||
pageClass: 'login-page sidemenu-hidden',
|
pageClass: 'login-page sidemenu-hidden',
|
||||||
})
|
})
|
||||||
.when('/invite/:code', {
|
.when('/invite/:code', {
|
||||||
|
@ -20,7 +20,6 @@ $login-border: #8daac5;
|
|||||||
|
|
||||||
& .btn-primary {
|
& .btn-primary {
|
||||||
@include buttonBackground(#ff6600, #bc3e06);
|
@include buttonBackground(#ff6600, #bc3e06);
|
||||||
height: 40px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,18 +161,17 @@ select:-webkit-autofill:focus {
|
|||||||
transform: tranlate(0px, 0px);
|
transform: tranlate(0px, 0px);
|
||||||
transition: 0.25s ease;
|
transition: 0.25s ease;
|
||||||
|
|
||||||
&.add {
|
|
||||||
transform: translate(0px, -320px);
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.remove {
|
&-enter {
|
||||||
transform: translate(0px, 320px);
|
transform: translate(0px, 320px);
|
||||||
&.hidden {
|
display: flex;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-enter-active {
|
||||||
|
transform: translate(0px, 0px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,15 +317,15 @@ select:-webkit-autofill:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-button-group {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
.login-inner-box {
|
.login-inner-box {
|
||||||
width: 55%;
|
width: 55%;
|
||||||
padding: $space-md 56px;
|
padding: $space-md 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-button-group {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button-forgot-password {
|
.login-button-forgot-password {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
Loading…
Reference in New Issue
Block a user