浏览代码

ChangePassword: Rewrite change password page to react (#17811)

* ChangePassword to React, created PasswordInput component, attempting UserProvider wrapper component, adding flex to btn row

* UserAPI interface, force classes on PasswordInput, remove api call from ChangePassword

* refactored out form

* clean up

* removed unnecessary bind, added loading state and loading component to change password form

* should be OR

* arrow funcs instead of binds, inline-block instead of flex, isSaving instead of isLoading, disabled button instead of spinner

* inline-flex on the react btn

* change state instatiatiation
Shavonn Brown 6 年之前
父节点
当前提交
874b8abcc0

+ 1 - 1
packages/grafana-ui/src/components/Button/AbstractButton.tsx

@@ -120,7 +120,7 @@ const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonV
   return {
     button: css`
       label: button;
-      display: flex;
+      display: inline-flex;
       align-items: center;
       font-weight: ${fontWeight};
       font-size: ${fontSize};

+ 20 - 0
public/app/core/components/PasswordInput/PasswordInput.tsx

@@ -0,0 +1,20 @@
+import React, { ChangeEvent, forwardRef } from 'react';
+import { Input, FormLabel } from '@grafana/ui';
+
+export interface Props {
+  label: string;
+  value: string | undefined;
+  onChange: (value: string) => void;
+}
+
+export const PasswordInput = forwardRef<HTMLInputElement, Props>((props, ref) => (
+  <>
+    <FormLabel className="width-8">{props.label}</FormLabel>
+    <Input
+      className="gf-form-input max-width-22"
+      type="password"
+      onChange={(event: ChangeEvent<HTMLInputElement>) => props.onChange(event.target.value)}
+      value={props.value}
+    />
+  </>
+));

+ 51 - 0
public/app/core/utils/UserProvider.tsx

@@ -0,0 +1,51 @@
+import React, { PureComponent } from 'react';
+import { getBackendSrv } from '@grafana/runtime';
+
+export interface UserAPI {
+  changePassword: (ChangePassword: ChangePasswordFields) => void;
+}
+
+interface LoadingStates {
+  changePassword: boolean;
+}
+
+export interface ChangePasswordFields {
+  oldPassword: string;
+  newPassword: string;
+  confirmNew: string;
+}
+
+export interface Props {
+  children: (api: UserAPI, states: LoadingStates) => JSX.Element;
+}
+
+export interface State {
+  loadingStates: LoadingStates;
+}
+
+export class UserProvider extends PureComponent<Props, State> {
+  state: State = {
+    loadingStates: {
+      changePassword: false,
+    },
+  };
+
+  changePassword = async (payload: ChangePasswordFields) => {
+    this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: true } });
+    await getBackendSrv().put('/api/user/password', payload);
+    this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: false } });
+  };
+
+  render() {
+    const { children } = this.props;
+    const { loadingStates } = this.state;
+
+    const api = {
+      changePassword: this.changePassword,
+    };
+
+    return <>{children(api, loadingStates)}</>;
+  }
+}
+
+export default UserProvider;

+ 0 - 29
public/app/features/profile/ChangePasswordCtrl.ts

@@ -1,29 +0,0 @@
-import angular from 'angular';
-import config from 'app/core/config';
-
-export class ChangePasswordCtrl {
-  /** @ngInject */
-  constructor($scope, backendSrv, $location, navModelSrv) {
-    $scope.command = {};
-    $scope.authProxyEnabled = config.authProxyEnabled;
-    $scope.ldapEnabled = config.ldapEnabled;
-    $scope.navModel = navModelSrv.getNav('profile', 'change-password', 0);
-
-    $scope.changePassword = () => {
-      if (!$scope.userForm.$valid) {
-        return;
-      }
-
-      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(() => {
-        $location.path('profile');
-      });
-    };
-  }
-}
-
-angular.module('grafana.controllers').controller('ChangePasswordCtrl', ChangePasswordCtrl);

+ 79 - 0
public/app/features/profile/ChangePasswordForm.tsx

@@ -0,0 +1,79 @@
+import React, { PureComponent, MouseEvent } from 'react';
+import config from 'app/core/config';
+import { Button, LinkButton } from '@grafana/ui';
+import { ChangePasswordFields } from 'app/core/utils/UserProvider';
+import { PasswordInput } from 'app/core/components/PasswordInput/PasswordInput';
+
+export interface Props {
+  isSaving: boolean;
+  onChangePassword: (payload: ChangePasswordFields) => void;
+}
+
+export interface State {
+  oldPassword: string;
+  newPassword: string;
+  confirmNew: string;
+}
+
+export class ChangePasswordForm extends PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      oldPassword: '',
+      newPassword: '',
+      confirmNew: '',
+    };
+  }
+
+  onOldPasswordChange = (oldPassword: string) => {
+    this.setState({ oldPassword });
+  };
+
+  onNewPasswordChange = (newPassword: string) => {
+    this.setState({ newPassword });
+  };
+
+  onConfirmPasswordChange = (confirmNew: string) => {
+    this.setState({ confirmNew });
+  };
+
+  onSubmitChangePassword = (event: MouseEvent<HTMLInputElement>) => {
+    event.preventDefault();
+    this.props.onChangePassword({ ...this.state });
+  };
+
+  render() {
+    const { oldPassword, newPassword, confirmNew } = this.state;
+    const { isSaving } = this.props;
+    const { ldapEnabled, authProxyEnabled } = config;
+
+    if (ldapEnabled || authProxyEnabled) {
+      return <p>You cannot change password when ldap or auth proxy authentication is enabled.</p>;
+    }
+
+    return (
+      <form name="userForm" className="gf-form-group">
+        <div className="gf-form max-width-30">
+          <PasswordInput label="Old Password" onChange={this.onOldPasswordChange} value={oldPassword} />
+        </div>
+        <div className="gf-form max-width-30">
+          <PasswordInput label="New Password" onChange={this.onNewPasswordChange} value={newPassword} />
+        </div>
+        <div className="gf-form max-width-30">
+          <PasswordInput label="Confirm Password" onChange={this.onConfirmPasswordChange} value={confirmNew} />
+        </div>
+        <div className="gf-form-button-row">
+          <Button variant="primary" onClick={this.onSubmitChangePassword} disabled={isSaving}>
+            Change Password
+          </Button>
+          <LinkButton variant="transparent" href={`${config.appSubUrl}/profile`}>
+            Cancel
+          </LinkButton>
+        </div>
+      </form>
+    );
+  }
+}
+
+export default ChangePasswordForm;

+ 46 - 0
public/app/features/profile/ChangePasswordPage.tsx

@@ -0,0 +1,46 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { StoreState } from 'app/types';
+import { NavModel } from '@grafana/data';
+import { getNavModel } from 'app/core/selectors/navModel';
+import { UserProvider } from 'app/core/utils/UserProvider';
+import Page from 'app/core/components/Page/Page';
+import { ChangePasswordForm } from './ChangePasswordForm';
+
+export interface Props {
+  navModel: NavModel;
+}
+
+export class ChangePasswordPage extends PureComponent<Props> {
+  render() {
+    const { navModel } = this.props;
+    return (
+      <Page navModel={navModel}>
+        <UserProvider>
+          {({ changePassword }, states) => (
+            <Page.Contents>
+              <h3 className="page-sub-heading">Change Your Password</h3>
+              <ChangePasswordForm onChangePassword={changePassword} isSaving={states.changePassword} />
+            </Page.Contents>
+          )}
+        </UserProvider>
+      </Page>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState) {
+  return {
+    navModel: getNavModel(state.navIndex, `change-password`),
+  };
+}
+
+const mapDispatchToProps = {};
+
+export default hot(module)(
+  connect(
+    mapStateToProps,
+    mapDispatchToProps
+  )(ChangePasswordPage)
+);

+ 0 - 1
public/app/features/profile/all.ts

@@ -1,3 +1,2 @@
 import './ProfileCtrl';
-import './ChangePasswordCtrl';
 import './PrefControlCtrl';

+ 0 - 35
public/app/features/profile/partials/change_password.html

@@ -1,35 +0,0 @@
-<page-header model="navModel"></page-header>
-
-<div class="page-container page-body">
-	<h3 class="page-sub-heading">
-		Change your password
-	</h3>
-
-	<div ng-if="ldapEnabled || authProxyEnabled">
-		You cannot change password when ldap or auth proxy authentication is enabled.
-	</div>
-
-	<form name="userForm" class="gf-form-group" ng-hide="ldapEnabled || authProxyEnabled">
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Old Password</span>
-			<input class="gf-form-input max-width-21" type="password" required ng-model="command.oldPassword">
-		</div>
-
-		<div class="gf-form">
-			<span class="gf-form-label width-10">New Password</span>
-			<input class="gf-form-input max-width-21" type="password" required ng-minlength="4"  ng-model="command.newPassword">
-		</div>
-
-		<div class="gf-form">
-			<span class="gf-form-label width-10">Confirm Password</span>
-			<input class="gf-form-input max-width-21" type="password" required ng-minlength="4"  ng-model="command.confirmNew">
-		</div>
-
-		<div class="gf-form-button-row">
-			<button type="submit" class="btn btn-primary" ng-click="changePassword()">Change Password</button>
-			<a class="btn-text" href="profile">Cancel</a>
-		</div>
-	</form>
-
-</div>
-

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

@@ -3,6 +3,7 @@ import './ReactContainer';
 import { applyRouteRegistrationHandlers } from './registry';
 
 // Pages
+import ChangePasswordPage from 'app/features/profile/ChangePasswordPage';
 import ServerStats from 'app/features/admin/ServerStats';
 import AlertRuleList from 'app/features/alerting/AlertRuleList';
 import TeamPages from 'app/features/teams/TeamPages';
@@ -230,8 +231,10 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
       controllerAs: 'ctrl',
     })
     .when('/profile/password', {
-      templateUrl: 'public/app/features/profile/partials/change_password.html',
-      controller: 'ChangePasswordCtrl',
+      template: '<react-container />',
+      resolve: {
+        component: () => ChangePasswordPage,
+      },
     })
     .when('/profile/select-org', {
       templateUrl: 'public/app/features/org/partials/select_org.html',