浏览代码

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
Tobias Skarhed 6 年之前
父节点
当前提交
91a911b64e

+ 135 - 0
public/app/core/components/Login/ChangePassword.tsx

@@ -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 - 0
public/app/core/components/Login/LoginCtrl.tsx

@@ -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 - 0
public/app/core/components/Login/LoginForm.tsx

@@ -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 - 0
public/app/core/components/Login/LoginPage.tsx

@@ -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 - 0
public/app/core/components/Login/LoginServiceButtons.tsx

@@ -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 - 0
public/app/core/components/Login/UserSignup.tsx

@@ -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>
+  );
+};

+ 0 - 1
public/app/core/controllers/all.ts

@@ -1,5 +1,4 @@
 import './json_editor_ctrl';
-import './login_ctrl';
 import './invited_ctrl';
 import './signup_ctrl';
 import './reset_password_ctrl';

+ 0 - 145
public/app/core/controllers/login_ctrl.ts

@@ -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);

+ 0 - 115
public/app/partials/login.html

@@ -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>

+ 5 - 2
public/app/routes/routes.ts

@@ -30,6 +30,7 @@ import { route, ILocationProvider } from 'angular';
 
 // Types
 import { DashboardRouteInfo } from 'app/types';
+import { LoginPage } from 'app/core/components/Login/LoginPage';
 
 /** @ngInject */
 export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) {
@@ -285,8 +286,10 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
     })
     // LOGIN / SIGNUP
     .when('/login', {
-      templateUrl: 'public/app/partials/login.html',
-      controller: 'LoginCtrl',
+      template: '<react-container/>',
+      resolve: {
+        component: () => LoginPage,
+      },
       pageClass: 'login-page sidemenu-hidden',
     })
     .when('/invite/:code', {

+ 12 - 14
public/sass/pages/_login.scss

@@ -20,7 +20,6 @@ $login-border: #8daac5;
 
   & .btn-primary {
     @include buttonBackground(#ff6600, #bc3e06);
-    height: 40px;
   }
 }
 
@@ -162,18 +161,17 @@ select:-webkit-autofill:focus {
   transform: tranlate(0px, 0px);
   transition: 0.25s ease;
 
-  &.add {
-    transform: translate(0px, -320px);
-    &.hidden {
-      display: none;
-    }
+  &.hidden {
+    display: none;
   }
 
-  &.remove {
+  &-enter {
     transform: translate(0px, 320px);
-    &.hidden {
-      display: none;
-    }
+    display: flex;
+  }
+
+  &-enter-active {
+    transform: translate(0px, 0px);
   }
 }
 
@@ -319,15 +317,15 @@ select:-webkit-autofill:focus {
     }
   }
 
+  .login-button-group {
+    flex-direction: row;
+  }
+
   .login-inner-box {
     width: 55%;
     padding: $space-md 56px;
   }
 
-  .login-button-group {
-    flex-direction: row;
-  }
-
   .login-button-forgot-password {
     padding-top: 0;
     padding-left: 10px;